Why Is This An "Anti-Pattern" in React???
Adam Nathaniel Davis
Posted on February 21, 2020
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:
"How does one component access the information (especially, a state variable) that resides in another component?"
"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:
-
<App>
→ calls →<ContentArea>
-
<ContentArea>
→ calls →<MainContentArea>
-
<MainContentArea>
→ calls →<MyDashboard>
-
<MyDashboard>
→ calls →<MyOpenTickets>
-
<MyOpenTickets>
→ calls →<TicketTable>
-
<TicketTable>
→ calls a series of →<TicketRow>
s - 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;
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/>;
}
}
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>;
}
}
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:
When the
<ContentArea>
component was loaded, it added a reference to itself into the sharedcomponents
object.When the
<TicketDetail>
component was loaded, it imported the sharedcomponents
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/>;
}
}
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>;
}
}
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/>;
}
}
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>
</>
);
}
}
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/>;
}
}
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}/>
</>
);
}
}
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>
</>
);
}
}
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:
<ContentArea>
is mounted-and-rendered.During this process, a reference to
<ContentArea>
is loaded into thecomponents
object.This eventually leads to
<TicketTable>
being mounted-and-rendered.During this process, a reference to
<TicketTable>
is loaded into thecomponents
object.This eventually leads to
<TicketDetail>
being mounted-and-rendered.The user is then shown the 'Toggle reduxSucks'
<button>
.The user clicks the 'Toggle reduxSucks'
<button>
.This calls the
toggleReduxSucks()
function that lives in the<ContentArea>
component.This, in turn, calls the
incrementReduxSucksHasBeenToggledXTimes()
function in the<TicketTable>
component.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 thecomponents
object. And when<ContentArea>
'stoggleReduxSucks()
function is called, it will be able to find a reference to<TicketTable>
'sincrementReduxSucksHasBeenToggledXTimes()
function in thecomponents
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 thecomponents
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 thecomponents
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 thecomponents
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 (viasetState()
), you must call asetState()
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 thecomponents
cache. In fact, you can carefully choose which functions/classes are dropped into thecomponents
cache, and which functions/classes try to reference something that's been dropped into thecomponents
cache. If you're writing a pure utility function/class, it's probably a very poor idea to leverage mycomponents
cache solution. Because using thecomponents
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.
Posted on February 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.