- Component library development on React + Storybook
- Test Driven Development in JS or How to Start Loving Programming
- Migrating a real project from Javascript to Typescript - pains and features
Now let's move on to the article.
When I started learning React, there were a few things that I didn't understand. And I think almost everyone who is familiar with React is asking the same questions. I am sure of this because people create entire libraries to solve pressing problems. Here are two main questions that almost every React developer seems to care about:
How does one component access information (especially a state variable) that is in another component? How does one component call a function that is in another component?
JavaScript developers in general (and React developers in particular) have been increasingly gravitating towards writing so-called pure functions lately. Functions that are not associated with state changes. Functions that don't need external database connections. Functions that are independent of what happens outside of them.
Of course, pure functions are a noble goal. But if you are developing a more or less complex application, then you will not be able to make every function clean. There will certainly come a time when you have to create at least a few components that are somehow related to other components. Trying to avoid this is ridiculous. These connections between components are called dependencies .
In general, dependencies are bad and are best used only when needed. But then again, if your application has grown, some of its components will necessarily depend on each other. Of course, the React developers know this, so they figured out how to get one component to pass critical information, or functions, to its child components.
Standard approach: use props to pass values
Any state value can be passed to another component through props. Any function can be passed to child components all through the same props. This is how descendants know what state values are stored up the tree and can potentially invoke actions in parent components. All this, of course, is good. But React developers are concerned about a particular problem.
Most applications are layered. In complex applications, structures can be nested very deeply. The general architecture might look something like this:
App
→ refers to → ContentArea
ContentArea
→ refers to → MainContentArea
MainContentArea
→ refers to → MyDashboard
MyDashboard
→ refers to → MyOpenTickets
MyOpenTickets
→ refers to → TicketTable
TicketTable
→ refers to a sequence → TicketRow
Every
TicketRow
→ refers to →TicketDetail
Theoretically, this garland can be wrapped around for a long time. All components are part of the whole. More precisely, part of a hierarchy. But here the question arises:
Can the component
TicketDetail
in the example above read the state values that are stored in ContentArea
? Or. Can a component TicketDetail
call functions that are in ContentArea
?
The answer to both questions is yes. In theory, all descendants can know about all the variables that are stored in the parent components. They can also call ancestor functions - but with a big caveat. This is possible only if such values (state or function values) are explicitly passed to descendants through props. Otherwise, the component's state or function values will not be available to its child component.
In small applications and utilities, this does not play a special role. For example, if a component
TicketDetail
needs to access state variables that are stored in TicketRow
, it is enough to make the component TicketRow
→ pass these values to its descendant → TicketDetail
through one or more props. The same is the case when a component TicketDetail
needs to call a function that is in TicketRow
. Component TicketRow
→ will pass this function to its descendant → TicketDetail
via prop. The headache begins when a component far down the tree needs to access the state or function of the component at the top of the hierarchy.
To solve this problem, React has traditionally passed variables and functions down all levels. But this clutters up the code, takes up resources and requires serious planning. We would have to pass values to many levels like this:
ContentArea
→ MainContentArea
→ MyDashboard
→ MyOpenTickets
→ TicketTable
→ TicketRow
→ TicketDetail
That is, in order to pass a state variable from
ContentArea
to TicketDetail
, we need to do a lot of work. Experienced developers understand that there is an ugly long chain of passing values and functions in the form of props through intermediate levels of components. The solution is so cumbersome that I even gave up learning React a couple of times because of it.
The monster named Redux
I'm not the only one who thinks that passing all state values and all functions common to components through props is very impractical. You’re unlikely to find any complex React application that doesn’t come with a state management tool. There are not so few such tools. Personally, I love MobX. Unfortunately, Redux is considered the "industry standard".
Redux is the brainchild of the creators of the React core. That is, they first created a wonderful React library. But they immediately realized that with her means it was almost impossible to manage the state. If they hadn't found a way to solve the inherent problems of this (otherwise great) library, many of us would never have heard of React.
So they came up with Redux.
If React is Mona Lisa, then Redux is a mustache attached to it. If you are using Redux, you will have to write a ton of boilerplate code in almost every project file. Troubleshooting and reading code becomes hell. Business logic is taken out to the backyard. The code contains confusion and vacillation.
But if developers have a choice: React + Redux or React without any third-party state management tools , they almost always choose React + Redux. Since the Redux library was developed by the React core authors, it is considered an approved solution by default. And most developers prefer to use solutions that have been tacitly approved like this.
Of course Redux will create a whole web of dependenciesin your React application. But, in fairness, any generic state management tool will do the same. The state management tool is a shared repository of variables and functions. Such functions and variables can be used by any component that has access to the shared storage. This has one obvious drawback: all components become dependent on shared storage.
Most of the React developers I know who have tried to resist using Redux eventually gave up. (Because ... resistance is useless.) I know a lot of people who immediately hate Redux. But when they were faced with the choice - Redux or "we'll find another React developer" - they threw themselveshave agreed to embrace Redux as an integral part of their lives. It's like taxes. Like a rectal exam. Like going to the dentist.
Reacting Shared Values in React
I'm too stubborn to give up so easily. After looking at Redux, I realized that I needed to look for other solutions. I can use Redux. And I worked in teams that used this library. In general, I understand what she does. But that doesn't mean I like Redux.
As I said before, if you can't do without a separate state management tool, then MobX is about ... a million times better than Redux! But I am tormented by a more serious question. It touches the collective mind of React developers:
Why do we always grab onto a state management tool first?
When I first started developing with React, I spent many nights looking for alternative solutions. And I found a way that many React developers neglect, but none of them can tell why . Will explain.
Imagine that in the hypothetical application I wrote about above, we create a file like this:
// components.js
let components = {};
export default components;
And that's all. Just two short lines of code. We create an empty object - good old JS object . We export it by default with
export default
.
Now let's see what the code inside the component might look like
<
ContentArea>
:
// 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, it looks like a perfectly normal class-based React component. We have a simple function
render()
that accesses the next component down the tree. We have a small function console.log()
that prints the result of code execution to the console, and a constructor. But ... there are some nuances in the constructor .
At the very beginning, we imported a simple object
components
. Then, in the constructor, we added a new property to the object components
with the name of the current React component ( this
). In this property we refer to the component this
. Now, every time we access the components object, we will have direct access to the component <
ContentArea>
.
Let's see what happens at the bottom of the hierarchy. The component
<
TicketDetail>
can be like this:
// 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>;
}
}
Here's what happens. Each time the component
TicketDetail
is rendered consoleLog()
, a function that is stored in the component will be called ContentArea
.
Note that the function is
consoleLog()
not passed through the entire hierarchy through props. In fact, the function is consoleLog()
not passed anywhere — not anywhere — to any component.
And yet it
TicketDetail
can call the function consoleLog()
that is stored in ContentArea
, because we did two things:
ContentArea
When loaded, the component added a link to itself to the components shared object.TicketDetail
When loaded, the component imported a shared objectcomponents
, that is, it had direct access to the componentContentArea
, despite the fact that properties wereContentArea
not passed to the componentTicketDetail
through props.
This approach doesn't only work with functions / callbacks. It can be used to directly query the values of state variables. Let's imagine what
<
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>
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>;
}
}
Now, every time the component is rendered
<
TicketDetail
, it will look for the value of the variable state.reduxSucks
in <
ContentArea>
. If the variable returns a value true
, the function console.log()
will print a message to the console. This will happen even if the value of the variable ContentArea.state.reduxSucks
has never been passed down the tree - to any of the components - via props. So, with one simple underlying JS object that lives outside of the standard React lifecycle, we can make it so that any descendant can read state variables directly from any parent loaded into the components object. We can even call functions of the parent component in its descendant.
The ability to call a function directly in child components means that we can change the state of parent components directly from their children. For example like this.
First, in the component,
<
ContentArea>
we will create a simple function that changes the value of a variable 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 component,
<
TicketDetail>
we'll call this method through the object components
:
// 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, after each rendering of a component, the
<
TicketDetail>
user will be able to press a button that will change (toggle) the value of the variable ContentArea.state.reduxSucks
in real time, even if the function ContentArea.toggleReduxSucks()
has never been passed down the tree through props.
With this approach, the parent component can call the function directly from its child. Here's how to do it. The updated component
<
ContentArea>
will 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/>;
}
}
Now let's add logic to the component
<
TicketTable>
. 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}/>
</>
);
}
}
As a result, the component has
<
TicketDetail>
not changed. 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>
</>
);
}
}
Have you noticed the oddity associated with these three classes? In the hierarchy of our application
ContentArea
, this is the parent component for TicketTable
, which is the parent component for TicketDetail
. This means that when we mount a component ContentArea
, it does not yet "know" about its existence TicketTable
. And the function toggleReduxSucks()
written in ContentArea
implicitly calls the child function:. It
incrementReduxSucksHasBeenToggledXTimes()
turns out that the code will not work , right?
But no.
Look. We have created several levels in the application, and there is only one way to call the function
toggleReduxSucks()
. Like this.
- We mount and render
ContentArea
. - During this process, a reference to the components is loaded into the components object
ContentArea
. - The result is mounted and rendered
TicketTable
. - During this process, a reference to the components is loaded into the components object
TicketTable
. - The result is mounted and rendered
TicketDetail
. - « reduxSucks» (Toggle reduxSucks).
- « reduxSucks».
-
toggleReduxSucks()
,ContentArea
. -
incrementReduxSucksHasBeenToggledXTimes()
TicketTable
. - , , « reduxSucks»,
TicketTable
components.toggleReduxSucks()
ContentArea
incrementReduxSucksHasBeenToggledXTimes()
,TicketTable
, components.
It turns out that the hierarchy of our application allows us to add an
ContentArea
algorithm to the component that will call a function from the child component, despite the fact that the component ContentArea
did not know about the existence of the component when it was mountedTicketTable
.
Wealth Management Tools - Dump
As I explained, I am deeply convinced that Redux is no match for MobX. And when I get the privilege of working on a project from scratch (unfortunately not often), I always campaign for MobX. Not for Redux. But when I develop my own applications , I rarely use third-party state management tools at all - almost never . Instead, I just cache objects / components whenever possible. And if this approach doesn't work, I often fall back to the default solution in React, that is, I just pass functions / state variables through props.
Known "issues" with this approach
I am well aware that my idea of caching the underlying object is
components
not always suitable for solving shared state / function problems. Sometimes this approach can ... play a cruel joke . Or it might not work at all . Here's something to keep in mind.
- It works best with singles .
For example, in our hierarchy, the <TicketTable> component contains <TicketRow> components with a zero-to-many relationship. If you want to cache the reference to each potential component inside the <TicketRow> components (and their child <TicketDetail> components) in the components cache, you have to store them in an array, and this can get tricky. I have always avoided this. - components , / , components. .
, . , , . / , , components. - ,
components
, (setState()
),setState()
, .
Now that I have explained my approach and some of its limitations, I must warn you. Since I discovered this approach, I have been sharing it with people who consider themselves professional React developers. Each time they answer the same thing:
Hmm ... Don't do that. They frown and act like I just messed up the air. Something in my approach seems to them ... wrong . At the same time, no one has yet explained to me, based on their rich practical experience, what exactly is wrong. It's just that everyone considers my approach ... blasphemy .
Therefore, even if you like this approach or if it seems convenient to you in some situations, I do not recommendtalk about it in an interview if you want to get a job as a React developer. I think that even just talking to other React developers, you need to think a million times before talking about this method, or maybe it's better not to say anything at all.
I've found that JS developers - and React developers in particular - can be overly categorical . Sometimes they do explain why Approach A is “wrong” and Approach B is “right”. But in most cases, they just look at a piece of code and declare it “bad,” even if they themselves cannot explain why.
So why is this approach so annoying for React developers?
As I said, none of my colleagues could reasonably answer why my method is bad. And if anyone is willing to honor me with an answer, it is usually one of the following excuses (there are few of them).
- , .
.... , , Redux ( MobX, ) / React-. , . — . , /, . : ,components
. , /components
, / , components. /,components
, components . , , . , , Redux, MobX, - . - React « ». … .
… . ? , . — - « » « », , , . React, . , . . « », . , React 100 %, ( ) , .
, ?
I wrote this post because I have been using this approach for years (in personal projects). And it works great . But every time I climb out of my personal bubble and try to have an intelligent conversation about this approach with other third - party React developers, I only come across categorical statements and silly judgments about "industry standards". Is
this approach really bad ? Well, really. I want to know. If this is indeed an "anti-pattern," I will be immensely grateful to those who will justify its incorrectness. The answer "I'm not used to this" will not suit me. No, I am not obsessed with this method. I am not suggesting that this is a panacea for React developers. And I admit that it doesn't work for all situations. But maybecan anyone explain to me what's wrong with it?
I really want to know your opinion on this matter - even if you blow me to smithereens.