DEV Community

Cover image for Why Is This An "Anti-Pattern" in React???
Adam Nathaniel Davis
Adam Nathaniel Davis

Posted on • Edited on

Why Is This An "Anti-Pattern" in React???

When I first started learning React, I had some initial confusion. In fact, I think that almost anyone who's done React wrestles with the same questions. I know this, because people have built entire libraries to address these concerns. Two of these main issues, that seem to strike nearly every budding React dev, are:

  1. "How does one component access the information (especially, a state variable) that resides in another component?"

  2. "How does one component invoke a function that resides in another component?"

JavaScript devs, in general, (and React devs, in particular) have become ever-more focused in recent years on writing "pure" functions. Functions that aren't intertwined with state changes. Functions that don't require outside connections to databases. Functions that don't require knowledge of anything happening outside the function.

A focus on "pure" functions is certainly a noble goal. But if you're building any application of a reasonable size and scope, there's just no way to make every function "pure". At some point, it becomes ridiculous to architect an application where at least some of the components are not inherently aware of what some of the other components in the application are doing. These strands of interconnectedness are commonly known as dependencies.

In general, dependencies are a bad thing, and it's wise to introduce them only when necessary. But again, if your app has grown to a "certain size", it's inevitable that at least some of your components will be be dependent upon one another. Of course, the React devs understood this, so they provided a basic means by which one component could pass critical information, or functions, down to its children.

The Default Approach of Passing Values by Props

Any state value can be passed to another component by props. Any functions can be passed down via those same props. This gives child components a way to become "aware" of state values that are stored higher up the chain. And it also gives them the potential to invoke actions on the parent components. This is all fine-and-good. But it doesn't take long before new React devs start to worry about a specific, potential "problem".

Most apps are built with some degree of "layering". In larger apps, this layering can be quite deeply-nested. A common architecture might look something like this:

  1. <App>→ calls →<ContentArea>
  2. <ContentArea>→ calls →<MainContentArea>
  3. <MainContentArea>→ calls →<MyDashboard>
  4. <MyDashboard>→ calls →<MyOpenTickets>
  5. <MyOpenTickets>→ calls →<TicketTable>
  6. <TicketTable>→ calls a series of →<TicketRow>s
  7. Each <TicketRow>→ calls →<TicketDetail>

In theory, this daisy chain could go on for many more levels. All the components are part of a coherent whole. Specifically, they're part of a hierarchy. But here's the key question:

In the example above, can a <TicketDetail> component read the state values that are in <ContentArea>? Or... can a <TicketDetail> component invoke functions that reside in <ContentArea>?


The answer to both questions is, yes. In theory, all of the descendants can be aware of all variables stored in their ancestors. And they can invoke the functions of their ancestors - with one big caveat. In order for that to work, those values (either state values or functions) must be explicitly passed down as props. If they aren't, then the descendant component has no awareness of the state values, or functions, that are available on the ancestor.

In minor apps or utilities, this may not feel like much of a hurdle. For example, if <TicketDetail> needs to query the state variables that reside in <TicketRow>, all that must be done is to ensure that <TicketRow>→ passes those values down to →<TicketDetail> in one-or-more props. The same is true if <TicketDetail> needs to invoke a function on <TicketRow>. <TicketRow>→ would just need to pass that function down to →<TicketDetail> as a prop. The headache occurs when some component waaayyy down the tree needs to interact with the state/functions that otherwise live much high-up in the hierarchy.

The "traditional" React approach to that problem is to solve it by passing the variables/functions all the way down through the hierarchy. But this creates a lot of unwieldy overhead and a great deal of cognitive planning. To do this the "default" way in React, we would have to pass values through many different layers, like so:

<ContentArea><MainContentArea><MyDashboard><MyOpenTickets><TicketTable><TicketRow><TicketDetail>

That's a lot of extra work just so we can get a state variable from <ContentArea> all the way down to <TicketDetail>. Most senior devs quickly realize that this would create a ridiculously-long chain of values and functions constantly getting passed, through props, through a great many intermediary levels of components. The solution feels so needlessly clunky that it actually stopped me from picking up React the first couple of times that I tried diving into the library.

A Giant Convoluted Beast Named Redux

I'm not the only one who thinks it's highly impractical to pass all of your shared state values, and all of your shared functions, through props. I know this, because it's nearly impossible to find any sizable React implementation that doesn't also make use of a bolted-on appendage known as a state-management tool. There are many out there. Personally, I love MobX. But unfortunately, the "industry standard" is Redux.

Redux was created by the same team that built the core React library. In other words, the React team made this beautiful tool. But almost immediately realized that the tool's inherent method for sharing state was borderline unmanageable. So if they didn't find some way to work around the inherent obstacles in their (otherwise beautiful) tool, it was never going to gain widespread adoption.

So they created Redux.

Redux is the mustache that's painted on React's Mona Lisa. It requires a ton of boilerplate code to be dumped into nearly all of the project files. It makes troubleshooting and code-reading far more obtuse. It sends valuable business logic into far-off files. It's a bloated mess.

But if a team is faced with the prospect of using React + Redux, or using React with no third-party state-management tool at all, they will almost always choose React + Redux. Also, since Redux is built by the core React team, it carries that implicit stamp of approval. And most dev teams prefer to reach for any solution that has that kind of implicit approval.

Of course, Redux also creates an underlying web of dependencies in your React application. But to be fair, any blanket state-management tool will do the same. The state-management tool serves as a common store in which we can save variables and functions. Those variables and functions can then be used by any component with access to the common store. The only obvious downside, is that now, every component is dependent upon that common store.

Most React devs I know have given up on any Redux resistance they initially felt. (After all... resistance is futile.) I've met plenty of guys who outright hated Redux, but faced with the prospect of using Redux - or not having a React job - they took their soma, drank their Kool-Aid, and now they've just come to accept that Redux is a necessary part of life. Like taxes. And rectal exams. And root canals.

Rethinking Shared Values in React

I'm always a little too stubborn for my own good. I took one look at Redux and knew that I had to look for better solutions. I can use Redux. I've worked on teams where it was used. I understand what it's doing. But that doesn't mean that I enjoy that aspect of the job.

As I've already stated, if a separate state-management tool is absolutely needed, then MobX is about, oh... a million times better than Redux. But there's a deeper question that really bothers me about the hive-mind of React devs:

Why are we constantly reaching for state-management tools in the first place??


You see, when I first started React development, I spent a number of nights at home playing around with alternative solutions. And the solution I found is something that many other React devs seem to scoff at - but they can't really tell me why. Let me explain:

In the putative app that was outlined above, let's say that we create a separate file that looks like this:

// components.js
let components = {};
export default components;
Enter fullscreen mode Exit fullscreen mode

That's it. Just two little lines of code. We're creating an empty object - a plain ol' JavaScript object. Then we're setting it up as the export default in the file.

Now let's see what the code might look like inside the <ContentArea> component:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      components.ContentArea = this;
   }

   consoleLog(value) {
      console.log(value);
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

For the most part, this looks like a fairly "normal" class-based React component. We have a simple render() function that's calling the next component below it in the hierarchy. We have a little demo function that does nothing but send some value to console.log(), and we have a constructor. But... there's something just a little bit different in that constructor.

At the top of the file, notice that we imported that super-simple components object. Then, in the constructor, we added a new property to the components object with the same name as this React component. In that property, we loaded a reference to this React component. So... from here out, anytime we have access to the components object, we'll also have direct access to the <ContentArea> component.

Now let's go way down to the bottom of the hierarchy and see what <TicketDetail> might look like:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      components.ContentArea.consoleLog('it works');
      return <div>Here are the ticket details.</div>;
   }
}
Enter fullscreen mode Exit fullscreen mode

So here's what's happening. Every time the <TicketDetail> component is rendered, it will call the consoleLog() function that exists in the <ContentArea> component. Notice that the consoleLog() function was not passed all the way through the hierarchy chain via props. In fact the consoleLog() function was not passed anywhere - at all - to any component.

And yet, <TicketDetail> is still capable of invoking <ContentArea>'s consoleLog() function because two necessary steps were fulfilled:

  1. When the <ContentArea> component was loaded, it added a reference to itself into the shared components object.

  2. When the <TicketDetail> component was loaded, it imported the shared components object, which meant that it had direct access to the <ContentArea> component, even though <ContentArea>'s properties were never passed down to <TicketDetail> through props.

This doesn't just work with functions/callbacks. It can also be used to directly query the value of state variables. Let's imagine that <ContentArea> looks like this:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

Then we can write <TicketDetail> as so:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return <div>Here are the ticket details.</div>;
   }
}
Enter fullscreen mode Exit fullscreen mode

So now, every time <TicketDetail> is rendered, it will look to see the value of <ContentArea>'s state.reduxSucks variable. And, if the value is true, it will console.log() the message. It can do this even though the value of ContentArea.state.reduxSucks was never passed down - to any component - via props. By leveraging one, simple, base-JavaScript object that "lives" outside the standard React life cycle, we can now empower any of the child components to read state variables directly from any parent component that's been loaded into the components object. We can even use that to invoke a parent's functions in the child component.

Because we can directly invoke functions in the ancestor components, this means that we can even influence parent state values directly from the child components. We would do that like this:

First, in the <ContentArea> component, we create a simple function that toggles the value of reduxSucks.

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

Then, in the <TicketDetail> component, we use our components object to invoke that method:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

Now, every time the <TicketDetail> component is rendered, it will give the user a button. Clicking the button will actually update (toggle) the value of the ContentArea.state.reduxSucks variable in real-time. It can do this even though the ContentArea.toggleReduxSucks() function was never passed down through props.

We can even use this approach to allow an ancestor component to directly call a function on one of its descendants. Here's how we would do that:

The updated <ContentArea> component would look like this:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
      components.TicketTable.incrementReduxSucksHasBeenToggledXTimes();
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

And now we're going to add logic in the <TicketTable> component that looks like this:

// ticket.table.js
import components from './components';
import React from 'react';
import TicketRow from './ticket.row';

export default class TicketTable extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucksHasBeenToggledXTimes: 0 };
      components.TicketTable = this;
   }

   incrementReduxSucksHasBeenToggledXTimes() {
      this.setState((previousState, props) => {
         return { reduxSucksHasBeenToggledXTimes: previousState.reduxSucksHasBeenToggledXTimes + 1};
      });      
   }

   render() {
      const {reduxSucksHasBeenToggledXTimes} = this.state;
      return (
         <>
            <div>The `reduxSucks` value has been toggled {reduxSucksHasBeenToggledXTimes} times</div>
            <TicketRow data={dataForTicket1}/>
            <TicketRow data={dataForTicket2}/>
            <TicketRow data={dataForTicket3}/>
         </>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

And finally, our <TicketDetail> component remains unchanged. It still looks like this:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

Now, you may spot something odd about these three classes. In our application hierarchy, <ContentArea>→ is an ancestor of →<TicketTable>→ which in turn is an ancestor of →<TicketDetail>. This means that when <ContentArea> is mounted, it will (initially) have no "knowledge" of <TicketTable>. And yet, inside <ContentArea>'s toggleReduxSucks() function, there's an implicit call to a descendant's function: incrementReduxSucksHasBeenToggledXTimes(). So this will break, right???

Umm... no.

You see, given the layers that we've created in the app, there is only one "path" through the app in which toggleReduxSucks() can be called. It goes like this:

  1. <ContentArea> is mounted-and-rendered.

  2. During this process, a reference to <ContentArea> is loaded into the components object.

  3. This eventually leads to <TicketTable> being mounted-and-rendered.

  4. During this process, a reference to <TicketTable> is loaded into the components object.

  5. This eventually leads to <TicketDetail> being mounted-and-rendered.

  6. The user is then shown the 'Toggle reduxSucks' <button>.

  7. The user clicks the 'Toggle reduxSucks' <button>.

  8. This calls the toggleReduxSucks() function that lives in the <ContentArea> component.

  9. This, in turn, calls the incrementReduxSucksHasBeenToggledXTimes() function in the <TicketTable> component.

  10. This works because, by the time the user has a chance to click the 'Toggle reduxSucks' <button>, a reference to the <TicketTable> component will have already been loaded into the components object. And when <ContentArea>'s toggleReduxSucks() function is called, it will be able to find a reference to <TicketTable>'s incrementReduxSucksHasBeenToggledXTimes() function in the components object.

So you see, by leveraging the inherent hierarchy of our application, we can place logic in the <ContentArea> component that will effectively call a function in one of its descendant components, even though the <ContentArea> component wasn't yet aware of the <TicketTable> component at the time that it was mounted.

Throwing Out Your State-Management Tools

As I've already explained, I believe - deeply - that MobX is vastly superior to Redux. And whenever I have the (rare) privilege of working on a "green fields" project, I will always lobby hard for us to use MobX rather than Redux. But when I'm building my own apps, I rarely (if ever) reach for any third-party state-management tool at all. Instead, I frequently use this uber-simple object/component-caching mechanism wherever it's appropriate. And when this approach simply doesn't fit the bill, I often find myself reverting to React's "default" solution - in other words, I simply pass the functions/state-variables through props.

Known "Issues" With This Approach

I'm not claiming that my idea of using a basic components cache is the end-all/be-all solution to every shared-state/function problem. There are times when this approach can be... tricky. Or even, downright wrong. Here are some notable issues to consider:

  • This works best with singletons.
    For example, in the hierarchy shown above, there are zero-to-many <TicketRow> components inside the <TicketTable> component. If you wanted to cache a reference to each of the potential <TicketRow> components (and their child <TicketDetail> components) into the components cache, you'd have to store them in an array, and that could certainly become... confusing. I've always avoided doing this.

  • The components cache (obviously) works on the idea that we can't leverage the variables/functions from other components unless we know that they've already been loaded into the components object.
    If your application architecture makes this impractical, this could be a poor solution. This approaches is ideally suited to Single Page Applications where we can know, with certainty, that <AncestorComponent> will always be mounted before <DescendantComponent>. If you choose to reference the variables/functions in a <DescendantComponent> directly from somewhere within an <AncestorComponent>, you must ensure that the application flow would not allow that sequence to happen until the <DescendantComponent> is already loaded into the components cache.

  • Although you can read the state variables from other components that are referenced in the components cache, if you want to update those variables (via setState()), you must call a setState() function that lives in its associated component.

Caveat Emptor

Now that I've demonstrated this approach, and outlined some of the known restrictions, I feel compelled to spell out one major caution. Since I've "discovered" this approach, I've shared it, on several different occasions, with people who consider themselves to be certified "React devs". Every single time that I've told them about it, they always give me the same response:

Umm... Don't do that.


They wrinkle their nose and furrow their brow and look like I just unleashed a major fart. Something about this approach just seems to strike many "React devs" as being somehow... wrong. Granted, I have yet to hear anyone give me any empirical reason why it's (supposedly) "wrong". But that doesn't stop them from treating it like it's somehow... a sin.

So even if you like this approach. Or maybe you see it as being somehow "handy" in given situations. I wouldn't recommend ever pulling this out during a job interview for a React position. In fact, even when you're just talking to other "React devs", I'd be careful about how/if you choose to mention it at all.

You see, I've found that JS devs - and React devs, in particular - can be incredibly dogmatic. Sometimes they can give you empirical reasons why Approach A is "wrong" and Approach B is "right". But, more often than not, they tend to just view a given block of code and declare that it's somehow "bad" - even if they can't give you any substantive reason to back up their claims.

Why, Exactly, Does This Approach Irk Most "React Devs"???

As stated above, when I've actually shown this to other React colleagues, I've yet to receive any reasoned response as to why this approach is "bad". But when I do get an explanation, it tends to fall into one of these (few) excuses:

  • This breaks the desire to have "pure" functions and litters the application with tightly-coupled dependencies.
    OK... I get that. But the same people who immediately dismiss this approach, will happily drop Redux (or MobX, or any state-management tool) into the middle of nearly all of their React classes/functions. Now, I'm not railing against the general idea that, sometimes, a state-management tool is absolutely beneficial. But every state-management tool is, essentially, a giant dependency generator. Every time you drop a state-management tool into the middle of your functions/classes, you're essentially littering your app with dependencies. Please note: I didn't say that you should drop every one of your functions/classes into the components cache. In fact, you can carefully choose which functions/classes are dropped into the components cache, and which functions/classes try to reference something that's been dropped into the components cache. If you're writing a pure utility function/class, it's probably a very poor idea to leverage my components cache solution. Because using the components cache requires a "knowledge" of the other components in the application. If you're writing the kind of component that should be used in many different places of the app, or that could be used across many different apps, then you absolutely would not want to use this approach. But then again, if you're creating that kind of global-use utility, you wouldn't want to use Redux, or MobX, or any state-management tool inside the utility either.

  • This just isn't "the way" that you do things in React. Or... This just isn't industry-standard.
    Yeah... I've gotten that kinda response on several occasions. And quite frankly, when I get that response, it makes me lose a little bit of respect for the responder. I'm sorry, but if your only excuse is to fall back on vague notions of "the way", or to invoke the infinitely-malleable boogeyman of "industry standards", then that's just fuckin lazy. When React was first introduced, it didn't come "out of the box" with any state-management tools. But people started playing with the framework and decided that they needed additional state-management tools. So they built them. If you really wanna be "industry standard", just pass all of your state variables and all of your function callbacks through props. But if you feel like the "base" implementation of React doesn't suit 100% of your needs, then stop closing your eyes (and your mind) to any out-of-the-box thinking that isn't personally approved by Dan Abramov.

So What Say YOU???

I put up this post because I've been using this approach (in my personal projects) for years. And it's worked wonderfully. But every time I step out of my "local dev bubble" and try to have an intelligent discussion about it with other, outside React devs... I'm only met with dogma and mindless "industry standard" speak.

Is this approach truly bad??? Really. I want to know. But if it's really an "anti-pattern", I'd sincerely appreciate if someone can spell out some empirical reasons for its "wrongness" that go beyond "this isn't what I'm accustomed to seeing." I'm open-minded. I'm not claiming that this approach is some panacea of React development. And I'm more-than-willing to admit that it has its own limitations. But can anyone out there explain to me why this approach is just outright wrong???

I'd sincerely love any feedback you can provide and I'm genuinely looking forward to your responses - even if they're blatantly critical.

Top comments (25)

Collapse
 
pclundaahl profile image
Patrick Charles-Lundaahl

Hey Adam,

First off, I just want to say thanks for the article! I don't agree with your proposal, but it was a really interesting thought experiment to Rey to figure out why not. Redux can definitely feel like a lot of overhead, and pursuing alternatives to is awesome.

Second, if you're up for it, I'd love to hear your thoughts on my points below.

Okay, so that said, I can offer at least 3 concrete reasons why I don't believe this proposal is not scalable, mostly related to maintainability. These are my own thoughts, mostly gathered from experience.

1) You've created an implicit dependency on your application hierarchy being instantiated in a specific order. While this works great for small projects where you are the only dev, it tanks the maintainability because it requires anyone who works on it to have knowledge of that implicit dependency. This increases onboarding time, makes it harder to debug, and makes your application more brittle with regards to change.

2) The singleton component cache will exist before, and outlive, all of your components. This means that, when reasoning about your component, you now have to consider not only what the state of that cache is now, but also what it was like in the past. Again, probably fine for a small application, but this can quickly lead to very hard-to-trace bugs in a large application. This also results in components that are way harder to test, because you now have to manually tear down and set up the component cache before every test.

3) The singleton only holds a few components right now, but when you have even half a dozen devs working on a mid-sized application, the potential for naming collisions starts to increase pretty drastically. Now you have state that out lives your components and is directly changed by components from totally different contexts.

Some additional, more fluffy points:

  • You've created dependencies both up and down the hierarchy. This means that, if you want to change something, you now need to tear out twice the dependencies.

  • Using a solution that only works for singletons means that you need a second solution for everything else. Now, whenever a new dev opens a component, they first need to figure out which of the two state management systems it uses.

I would at least suggest that, rather than importing a singleton component cache, you pass your cache to child objects. It's just one argument instead of multiple. This allows you to separate contexts, makes testing easier, and at least hints to other devs that there's something extra they need to provide (though I would wager that just using React's context API is probably the best choice).

Thanks for the article! Even if I don't agree with your proposal, it's always cool to try to understand why. As said, I'd love to hear your thoughts.

Regards,
Patrick

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

I'm gonna circle back to this in a bit (hopefully, later today when I have some more time). But I wanted to drop a quick reply here to say thank you for these thoughts. It's already given me some ideas to chew on. And I've never been bothered by the fundamental idea that my technique might not be "best practice". I've been bothered by the fact that those who dismiss it have only bothered to reply with dogma and with vague notions of "industry standards" - without being able to actually say why something is-or-is-not an "industry standard".

Collapse
 
pclundaahl profile image
Patrick Charles-Lundaahl

No worries!

Thanks for letting me know - I always worry when posting these kind of responses that they're not going to land right.

Also, I totally agree with your stance on dogma - if you're using something simply because it's really popular, without understanding the pros and cons, you might as well be blindly copying code from Stack Overflow (actually, I think that might be the less harmful of the two).

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis • Edited

OK, now that I've had some time to work through some coding samples and figure some things out, I finally have time to put a "proper" reply on your well-considered points. And... I'm not going to.

Because the stuff I've figured out over the last 24 hours (with the awesome feedback of you and others on this post) really shouldn't be encapsulated in a single comment/reply. I'm gonna put this into a new blog posting.

But if you're curious about the Cliff's Notes version of what I've come up with:

  1. My previous approach may not have been outright wrong, but I definitely understand a lot of your points, and it makes the optimal use-case for my approach extremely narrow.
  2. This led me to reassess other approaches - including having another look at the Context API (which is what Redux uses under the covers).
  3. I'm now convinced, with the "new"/finalized Context API, that this will definitely be my go-to state-sharing/prop-passing tool of the future.

That's what I'm gonna explain in my next post...

Thread Thread
 
pclundaahl profile image
Patrick Charles-Lundaahl

Wow, thanks! I'm stoked you found my ramblings useful!

Your article was really thought-provoking. Also, respect for posting things you know people disagree with - that stuff terrifies me. Have fun with the Context API! I'm looking forward to your next post!

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis
Collapse
 
isaachagoel profile image
Isaac Hagoel

Haha.. this is one of the best articles I've read in quite some time. It is a cool solution. As you said, it is not suitable in all cases but I can totally see myself using it under the right circumstances. I am also bothered by the dogmatic aspect of the community... Try to criticize React Hooks and see what happens ;)

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Oh, man... Don't even get me started on Hooks. I'm sure I'll do another post on that one some day. Hooks are fine. Hooks are cool. But almost since the day they were announced, I've noticed that it inspires some kinda messianic fanboy complex in those who love them. It seems that they're incapable of seeing/using Hooks as a tool to be leveraged when appropriate. Instead, they start marching around and singing some hypnotic "Hooks Chant" as though merely saying the word "Hooks" will make all of their applications magical and bug-proof.

Collapse
 
ab6d profile image
ab6d

The context API exists as well and can be a solution :
reactjs.org/docs/context.html

Mind you, it doesn't seem suitable for high-frequency updates :
github.com/facebook/react/issues/1...
(but maybe it has been improved since?)

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

I agree that this "issue" is really what the Context API was designed for. Every time I've tried to dive in and use it myself, it just doesn't seem to "grok" properly in my brain. But that's not a React problem or a Context API problem. That's a me problem, and I just have to endeavor to make myself more comfortable with it so I can truly assess its faults-and-benefits.

Also... if you look under the covers, Redux basically is the Context API. That's what it's using.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Finally, I like how you point out the potential similarities with Redux (umm... sorta) and MobX (much closer). I'll freely admit that, in a "corporate" coding environment, if given the choice between using my custom technique or using MobX, I'd probably just use MobX. My technique and MobX both allow for a shared reference to functions (as opposed to serializable data).

But I wouldn't want to move the functions out of their original components. IMHO, if a component does "a thing", then I want to see all the functions that are specifically used to do "that thing" right there, in that component. But I freely admit that this bias is more of my own personal coding preference.

There is a practical reason for keeping the functions with their original components, however. That reason is: state management/updates. I've found that this technique can wander into bizarre territory if I'm trying to setState() in a given component - but the function that accomplishes the setState() is not in its original component.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

As for functional components, I freely admit that, in my "side" dev, I've still been writing a lotta "old school" class components. And this solution was built with that in mind. But I'm not entirely understanding why it simply "wouldn't work" with functional components? Granted, I'm not trying this in an IDE right now, but isn't the following valid?

import components from './components';
import React from 'react';

function DoSomething() {
   console.log(components.ContentArea.state.reduxSucks);
   return <div>Did it.</div>;
}

components.DoSomething = DoSomething;

Of course, you'd have to ensure that your app is designed in such a way that DoSomething() can never be invoked until after the <ContentArea> component's been mounted.

Collapse
 
murkrage profile image
Mike Ekkel

The answer still holds true, though. Simply because it's not the way React works.

That's not a bad thing, mind you. Just because it doesn't work that way, doesn't mean it couldn't work that way. That's how most things come to be. We use product A, B and C. Honestly, we just want a product that does some of the things from A, some of the things from B and some of the things from C combined. Now we've got the iPhone.

I see the answer as a very strong argument, which I'm willing to break down. It doesn't work that way, but I'll make it work that way! Then if it does work that way, and it works well, you can go back to the source and let it adopt the changes.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

I think it depends on how literal we're being about "it's not the way React works". I agree with you that, if the app/library/tool/whatever truly doesn't work that way, then it can absolutely be a fun and enriching challenge to figure out how it could work that way. But I feel the issue is that, often, when people say, "That's just not the way that this works" they're kinda fibbing about that. Many times, the technique does work that way - but the person claiming that it "doesn't work" is really saying, "This doesn't work with the way that I personally want to see code written." In other words, when some people utter those words, they're not referencing empirical truth. They're just using "it's not the way React works" as a cover for their own preferences - and their own dogma.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

As for scaling, this is a great point. In my experience, most of the "true" Single Page Applications don't get toooo big. At some point, you end up shunting the user to another application (even if it's designed to feel, to the user, like it's all the same application). I'm currently using this approach in an app that has 35k+ LoC, but I understand that a single SPA could, in theory, get far larger than that and the references could get tricky.

As to your solution, I totally agree on the potential to create a proxy object that would have at least some additional control as opposed to a plain ol' JS object. In fact, I know that I've thought about that idea before. I may even tinker with something like that in my free time today...

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

As for race conditions, I completely agree with you. That's why I have a particular application structure in mind when I feel like using this approach. Most of the time, I have some component that sits near/at the top of the hierarchy. Then I have descendants, nested much lower on the tree, that leverage the components object to access things that were higher in the tree. In this scenario, it's impossible for the descendant to be mounted before the ancestor, so it eliminates most of the race-condition concerns.

Granted, I didn't spell this out in the original post, but if I have sibling branches in that tree, I don't try to use this approach to "talk" between one sibling branch and the next. Instead, I use it to "talk" up (or, occasionally down) a given branch. But I can understand where this is a potential pitfall of my approach, because this "don't talk between branches" rule is something that's only been enforced in my head - and it's not in any way enforced by the technique itself.

 
bytebodger profile image
Adam Nathaniel Davis • Edited

I actually have this in my list as an item to blog about. The topic I'm referencing is the idea (that I've witnessed lately) amongst many React devs that we need to religiously separate our business logic and our display logic. The only issue I take with that is that, in most React applications, almost all the logic is display logic. And thus, IMHO, much of the logic actually belongs in the function/component where it's being rendered. Of course, there are exceptions to this. And I'm not saying that there's not a time-and-place to break out some of the business logic into separate components/functions/layers. But there are far too many people holding onto the old MVC edicts who think that damn near any "logic" should be washed out of React components. I... have a more nuanced opinion on such things.

Collapse
 
zenventzi profile image
Zen Ventzi

Nice article, but man.. I don't carry all my arguments with me all the time for every decision that I make, you know :) Not to mention that if you asked me on the spot, I'd have 10 other things that I'd be thinking about right there and then. Plus, when I'm working, I'm not consciously trying to recite every argument for every decision that I make. I "just know". It's intuitively based on hundreds of previous decisions that I've made. Think about that :)

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

This misses the whole point. Anyone who's been doing this for long enough has a stockpile of "rules of thumb" they use to make snap coding decisions. And they rarely sit there and "recite every argument for every decision" they make. I get that. No one disputes that.

But that was never the point of this article. The real question is: "When someone else is coding something in a way that you don't personally like, what reason do you give to explain why you don't like it, and why they should possibly feel compelled to change it?"

Do you just throw up some baseless bromide like, "Well, umm... it's, uhhh... It's an antipattern!" And then just cross your arms and smile, content in the idea that you've somehow quantified your objection? Or do you try to give the person tangible, empirical, logical reasons for why their approach might not be ideal??

If you think that someone else's coding is indeed an antipattern, that's fine. But if your sole explanation of the "problem" is to tell them that it's an antipattern, then, well... that's kinda messed up.

The specific practice I outlined in this article is a great example of this. Even with all of the great comments left on the article, I've yet to have anyone give me any tangible reason why this approach may be an "antipattern".

To be clear, some people have pointed out some downsides to this approach. But that doesn't make it an antipattern.

Too often in our career field, one dev tries to shout down another by hollering that something is a "code smell" or an "antipattern" - when they can't give you any specific reasons why the approach is supposedly suboptimal.

Collapse
 
zenventzi profile image
Zen Ventzi

I don't necessarily disagree, it really depends who is asking the question and who is answering. I definitely respect your passion for the craft, though. You obviously have the need to understand well what you do.

And about the other comments, for me, they are more than enough to call it an anti-pattern and leave it at that. But again it depends who that is coming from.

I've got colleagues whom I'd definitely challenge when they say something is an anti-pattern. I also know people who if asked the same question and say it's an anti-pattern, I'd stop there and start questioning myself instead of expecting them to answer to me.

So it's very contextual I'd say at the very least.