A React application is basically a tree of components that communicate data with each other. Passing data between components is often painless. However, as the app tree grows, it becomes harder to pass that data while maintaining a sound & readable codebase.
Say we have the following tree structure:
Here we have a simple tree with 3 levels. In this tree, node D and node E both manipulate some similar data: Say the user inputs some text in node D, which we wish to display in node E.
How do we pass that data from node D to node E?
The article presents 3 possible approaches to tackle this issue:
- Prop drilling
- Redux
- React's context API
The aim of the article is to compare these approaches and show that, when it comes to solving a common problem such as the one we just worded, it is possible to just stick with React's context API.
Approach 1: Prop drilling
A way of doing it would be to naively pass the data from child to parent then from parent to child through props as such: D->B->A then A->C->E.
The idea here is to use the onUserInput
function triggered from child to parent to carry the input data from node D to the state at node A, then we pass that data from the state at node A to node E.
We start with node D:
class NodeD extends Component {
render() {
return (
<div className="Child element">
<center> D </center>
<textarea
type="text"
value={this.props.inputValue}
onChange={e => this.props.onUserInput(e.target.value)}
/>
</div>
);
}
}
When the user types something, the onChange
listener will trigger the onUserInput
function from the prop and pass in the user input. That function in the node D prop will trigger another onUserInput
function in the node B prop as such:
class NodeB extends Component {
render() {
return (
<div className="Tree element">
<center> B</center>
<NodeD onUserInput={inputValue => this.props.onUserInput(inputValue)} />
</div>
);
}
}
Finally, when reaching the root node A, the onUserInput
triggered in the node B prop will change the state in node A to the user input.
class NodeA extends Component {
state = {
inputValue: ""
};
render() {
return (
<div className="Root element">
<center> A </center>
<NodeB
onUserInput={inputValue => this.setState({ inputValue: inputValue })}
/>
<NodeC inputValue={this.state.inputValue} />
</div>
);
}
}
That inputValue will then be through props from Node C to its child Node E:
class NodeE extends Component {
render() {
return (
<div className="Child element">
<center> E </center>
{this.props.inputValue}
</div>
);
}
}
See it already added some complexity to our code even if it's just a small example. Can you image how it would become when the app grows? 🤔
This approach relies on the number of depth of the tree, so for a larger depth we would need to go through a larger layer of components. This can be too long to implement, too repetitive and increases code complexity.
Approach 2: Using Redux
Another way would be to use a state management library like Redux.
Redux is a predictable state container for JavaScript apps.
The state of our whole application is stored in an object tree within a single store, which your app components depend on. Every component is connected directly to the global store, and the global store life cycle is independent of the components' life cycle.
We first define the state of our app: The data we interested in is what the user types in node D. We want to make that data available to node E. To do that, we can make that data available in our store. Node E can then subscribe to it in order to access the data.
We will come back to the store in a bit.
Step 1: Define Reducer
The next thing is to define our reducer. Our reducer specifies how the application's state changes in response to actions sent to the store. We define our reducer block as such:
const initialState = {
inputValue: ""
};
const reducer = (state = initialState, action) => {
if (action.type === "USER_INPUT") {
return {
inputValue: action.inputValue
};
}
return state;
};
Before the user has typed anything, we know that our state's data or inputValue will be an empty string. So we define a default initial state to our reducer with an empty string inputValue.
The logic here is once the user types something in node D, we "trigger" or rather dispatch an action and our reducer updates the state to whatever has been typed. By "update" here I do not mean "mutate" or change the current state, I do mean return a new state.
The if statement maps the dispatched action based on its type to the new state to be returned. So we already know that the dispatched action is an object containing a type key. How do we get the user input value for the new state ? We simply add another key called inputValue to our action object, and in our reducer block we make the new state's inputValue has that input value with action.inputValue
. So the actions of our app will follow this architecture:
{ type: "SOME_TYPE", inputValue: "some_value" }
Ultimately, our dispatch statement will look like this:
dispatch({ type: "SOME_TYPE", inputValue: "some_value" })
And when we call that dispatch statement from whatever component, we pass in the type of the action, and the user input value.
Okay, now we have an idea of how the app works: In our input node D we dispatch an action of type USER_INPUT
and pass in the value of whatever the user just typed, and in our display node E we pass in the value of the current state of the app aka the user input.
Step 2: Define the Store
In order to make our store available, we pass it in aProvider
component we import from react-redux. We then wrap our App inside of it. Since we know that nodes D and E will use the data in that store, we want our Provider component to contain a common parent of those nodes, so either root node A or our entire App component. Let us chose our App component to be contained in our Provider as such:
import reducer from "./store/reducer";
import { createStore } from "redux";
import { Provider } from "react-redux";
const store = createStore(reducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Now that we have set up our store and reducer, we can get our hands dirty with our nodes D and E !
Step 3: Implement user input logic
Let's first take a look at node D. We are interested in what the user inputs in the textarea
element. This means two things:
1- We need to implement the onChange
event listener and make it store whatever the user types in the store.
2- We need the value attribute of the textarea
to be the value stored in our store.
But before doing any of that, we need to set up a few things:
We first need to connect our node D component to our store. To do so, we use the connect()
function from react-redux. It provides its connected component with the pieces of the data it needs from the store, and the functions it can use to dispatch actions to the store.
This is why we use the two
mapStateToProps
andmapDispatchToProps
which deal with the store's state and dispatch respectively. We want our node D component to be subscribed to our store updates, as in, our app's state updates. This means that any time the app's state is updated,mapStateToProps
will be called. The results ofmapStateToProps
is an object which will be merged into our node D's component props. OurmapDispatchToProps
function lets us create functions that dispatch when called, and pass those functions as props to our component. We will make use of this by returning new function that callsdispatch()
which passes in an action.
In our case, for the mapStateToProps
function, we are only interested in the inputValue, so we return an object { inputValue: state.inputValue }
. For the mapDispatchToProps
, we return a function onUserInput
that takes the input value as parameter and dispatches an action of type USER_INPUT
with that value. The new state object returned by mapStateToProps
and the onUserInput
function are merged into our component's props. So we define our component as such:
class NodeD extends Component {
render() {
return (
<div className="Child element">
<center> D </center>
<textarea
type="text"
value={this.props.inputValue}
onChange={e => this.props.onUserInput(e.target.value)}
/>
</div>
);
}
}
const mapStateToProps = state => {
return {
inputValue: state.inputValue
};
};
const mapDispatchToProps = dispatch => {
return {
onUserInput: inputValue =>
dispatch({ type: "USER_INPUT", inputValue: inputValue })
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(NodeD);
We are done with our node D! Let's now move on to node E, where we want to display the user input.
Step 4: Implement user output logic
We wish to display the user input data on this node. We already know that this data is basically what is in the current state of our app, as in, our store. So ultimately, we wish to access that store and display its data. To do so, we first need to subscribe our node E component to the store's updates using the connect()
function with the same mapStateToProps
function we used before. After that, we simply need to access the data in the store from the props of the component using this.props.val as such:
class NodeE extends Component {
render() {
return (
<div className="Child element">
<center> E </center>
{this.props.val}
</div>
);
}
}
const mapStateToProps = state => {
return {
val: state.inputValue
};
};
export default connect(mapStateToProps)(NodeE);
And we're finally done with Redux! 🎉 You can take a look at what we just did here.
In the case of a more complex example, say with a tree with more components that share/manipulate the store, we would need those two mapStateToProps
and mapDispatchToProps
functions at each component. In this case, it might be wiser to separate our action types and reducers from our components by creating a separate folder for each.
…Who's got the time right?
Approach 3:Using React's context API
Now let's redo the same example using the context API.
The React Context API has been around for a while but only now in React's version 16.3.0 did it become safe to use in production. The logic here is close to Redux's logic: we have a context object which contains some global data that we wish to access from other components.
First we create a context object containing the initial state of our app as default state. We then create a Provider
and a Consumer
component as such:
const initialState = {
inputValue: ""
};
const Context = React.createContext(initialState);
export const Provider = Context.Provider;
export const Consumer = Context.Consumer;
Our
Provider
component has as children all the components from which we want to access the context data. Like theProvider
from the Redux version above. To extract or manipulate the context, we use our Consumer component which represents the component.
We want our Provider
component to wrap our entire App, just like in the Redux version above. However, this Provider
is a little different than the previous one we've seen. In our App component, we initialise a default state with some data, which we can share via value prop our Provider
component.
In our example, we're sharing this.state.inputValue along with a function that manipulates the state, as in, our onUserInput function.
class App extends React.Component {
state = {
inputValue: ""
};
onUserInput = newVal => {
this.setState({ inputValue: newVal });
};
render() {
return (
<Provider
value={{ val: this.state.inputValue, onUserInput: this.onUserInput }}
>
<div className="App">
<NodeA />
</div>
</Provider>
);
}
}
Now we can go ahead and access the data of our Provider
component using our Consumer component :)
For node D in which the user inputs data:
const NodeD = () => {
return (
<div className="Child element">
<center> D </center>
<Consumer>
{({ val, onUserInput }) => (
<textarea
type="text"
value={val}
onChange={e => onUserInput(e.target.value)}
/>
)}
</Consumer>
</div>
);
};
For node E in which we display the user input:
const NodeE = () => {
return (
<div className="Child element ">
<center> E </center>
<Consumer>{context => <p>{context.val}</p>}</Consumer>
</div>
);
};
And we're done with our context version of the example! 🎉 It wasn't that hard was it ? Check it out here
What if we have more components that we wish to be able to access the context? We can just wrap them with the Provider component and use the Consumer component to access/manipulate the context! Easy :)
Ok, but which one should I use
We can see that our Redux version of the example took a bit more time to do than our Context version. We can already see that Redux:
- Requires more lines of code and can be too "boilerplate" with a more complex example (more components to access the store).
- Increases complexity: It might be wiser to separate your reducer and action types from the components into unique folders/files when dealing with many components.
- Introduces a learning curve: Some developers find themselves struggling to learn Redux as it requires you to learn some new concepts: reducer, dispatch, action, thunk, middleware…
If you are working on a more complex App and wish to view a history of all the dispatched action by your app, "click" on any one of them and jump to that point in time, then definitely consider using Redux's pretty dope devTools extension!
However if you're only interested in making some data global to access it from a bunch of components, you can see from our example that Redux and React's context API both do roughly the same thing. So in a way, you don't have to use Redux!
Top comments (54)
Nice comparison! :-D
IMHO the key to make an application easier to maintain is testing. Many people forget about this but in the end needing more/less lines of code is not as important as having the best possible testing environment.
That's why we used redux and saga, it makes the application very easy to test if you follow its rules.
I'm very curious about how Redux provides a better testing environment. I feel like React evolved enough so that you can achieve anything you do with Redux, without it. Could you provide more details or examples?
Hello Seif! :-)
I don't think I said Redux provides a better testing environment. I cannot compare because I don't have the same experience using the Redux/Redux-Saga stack than using React Context API.
What I like about Redux and Saga is:
Maybe I misspoke when I said
the best possible testing environment
. I didn't mean mine, what I meant is testing is usually overlooked, and having a not optimal testing environment would lead to technical debt pretty quickly.I'd like to know more about React Context API,
useEffect
and the rest of the new React magic ✨, but what I've read didn't give me what I was looking for, do you have any examples regarding testing with React Context API so I could have a look?Thanks! 💯
Using the context API doesn't necessarily go against separating data. Redux & the Context make it both possible to solve that issue by making it possible to create both providers and consumers of data. So afaik whatever you do with Redux to separate "data flow", you can do it without Redux as well.
I found that the example in this article already shows a clear case where you can implement things without sacrificing a separation between the components and the data flow.
The reason I asked for examples is that I haven't yet seen a single concrete example that showed me something that only Redux can solve better 🤷♂️.
So if you do, please share it. Otherwise, I'm curious about what were you looking for in the new React features and that you didn't find? Examples of testing features?
Implementing isolated reusable components (e.g. a design system that you can showcase with storybook) can also be solved without using an external
state management library such as Redux.
It's important to test features, and not test for testing. Testing API calls or tiny pure functions that update the state will give you more green checks on the terminal for sure. However, it's a false sense of achievement. I believe that good tests focus on testing features and not implementation details such as API calls, sagas or event a React context. Testing features shouldn't rely on such implementation details.
At the end of the day, the user of our app doesn't care about how much "testing coverage" we have or whether we used Redux or React Context. The user only cares about how many bugs occur while using our application.
Hi Seif,
I'm at risk here as a new new dev (I didn't stutter), however I do think that your comment about testing appears to take a position that I'm not sure I understand from what I've learned and been told, nor did I get from the person you have responded too as well.
I believe that those tiny checks aren't necessarily about displacing the user from the center as it seems you have assumed. As I've learned it's about making it easier for a developer to reason around the tests. I'm assuming unit tests and not integration tests. That's a reasonable assumption I think, right?
I find it easier to read small tests and grasp what's being tested. This in turn means when I add code it's easier to either add tests, see a need for a tweak to a test or what have you. It's true that this may lead to an error ultimately, but I don't think one set of testing displaces the other. Do you think that?
Personally I enjoy testing and find unit tests to greatly assist me in solving the problem better when I write my code (I do TDD mainly), however I also do functional testing both with and without the UI rendered. Is this not the best practice? I thought it was and if I'm right I don't think it's reasonable your last point as it takes a point of view that one form or type of testing has hegemony over another.
I do love your other points btw. I'm trying to determine if I need to use Redux anymore or can I move to the new Context API as my first choice.
Thank you ! :D
Production code uses advance composition patterns and libaries like Redux for maintenance and testing. You don't have to use redux for your blog or a small app your building, but if you are building a real web app... use redux or another state management variant to scale your business effectively.
What we see is a lot of junior devs complaining about "unnessessary complexity" because redux has a steep learning curve. We need to look past this and let junior devs understand what real production code bases look like as they delve deeper into their careers and move up the corporate ladder or start something of their own
Very well said - this pretty much echoes my concerns. The notion that a state container adds complexity is somewhat misguided. It adds overhead and boilerplate - sure, but actually reduces complexity by making the application state transparent and predictable - an invaluable tool when debugging large codebases.
Good point regarding a "blog or small app" too - I have an article describing what happens when trying to build something of "moderate" size without proper state management not so long ago myself: Seriously - do React hooks replace state containers?
I would actually argue the exact opposite.
What I increasingly see is lot of mid-level devs mindlessly using tools just because they use them everywhere else regardless if their project actually needs it and establishing dictatorship over this because junior devs can't really challenge them. Usually when I see this in a company, at some point a senior dev comes in and almost always it results in mid-levels leaving project shortly after.
IMHO Redux is just an excuse to not having to think hard about your architecture.
Fundamental questions about development are like religion, you can never win discussing them.
Redux, scope models, Context etc. All been around for a long time and all are good tools to achieve end result, it is up to you to choose the right tool and use it well.
Nobody needs redux unless you have a really complex app, even then I’m not sure. Just use hooks; useState and useContext, it’s enough for most apps. Often less indirection, that’s not just junior’s view. There are imo very few use cases that require global/shared state. Especially when you use GraphQL with eg react-apollo.
imo start the state at the component, and ‘upgrade’ it up the tree depending on requirement, switching from state to context only once required
It's not as much about global/shared state, rather more about having state/logic tied to the component tree, which makes it painful, error prone and time consuming to make even simple layout changes. We end up moving state around the tree, instead of focusing on solving the real problems our users have.
Moreover, as the state end up sprinkled around the component tree here and there, it makes it even more difficult for new developers to come into this codebase and start doing something useful.
Don’t see how it can be more complex, if the state lives close to where it is used. To me it seems harder once you start adding more indirection.
I think you can have state relatively close to its usage (page container) while remaining flexible to make layout changes. Just need to separate state handling from presentational components. Also it depends, sometimes you have presentational state, that usually lives just fine deeper in the tree.
Making custom hooks or hocs that manage specific state or behavior makes it imo also easier to grasp as it’s more finegrained.
Not trying to suggest to apply chaos theory, structure is important.
True! Depending on who you ask, however, "page container" might not be considered "close to its usage". My concern is primarily about sprinkling state around individual components inside the page component tree.
Otherwise I completely agree - keeping state at the top level is conceptually similar to using a state container in that it achieves the same purpose.
True in traditional data by props based components, but not when you adopt components by props and children;
See my response at dev.to/patroza/comment/bcnf
Redux is not hard to learn, yes, it's a bit verbose, but I think about redux as a toolbox where you have more control over the app state. That implies more code to write sadly. On the other hand, redux can be overkill depending on the app.
In my opinion redux also has a very nice official documentation enough to get you started.
React context seems pretty neat and easy to use. At the end is always the correct tool for the job right?
Nice comparisons, thanks for sharing!
I see, thank you for sharing your opinion ! :)
Thank you for your article!
Redux has become way less verbose and intuitive after they introduced the Redux Started Kit. It feels like a modern and DX-friendly way to use Redux now, I recommend checking it out as the Redux ecosystem is great to build robust apps. redux-starter-kit.js.org
Great post!
We additionally use a HoC to make using the Context API easier. Something like this:
Then to use it ...
Would suggest to use
useContext
, either directly, or in the HOC:Yep you sure could! The original example was without hooks, so I went that route.
Thank so much for sharing this !!! :DD
You're welcome, hope it helps!
The Context.Consumer code scattered everywhere felt clunky.
Note that Sebastian Markbage, a member of the React team, has said Context is not suitable as a replacement for a Flux-based state management system (such as Redux) in the case where high frequency updates to the state are necessary.
See this blog post (close to the end), which references commentary on this GitHub thread about the release of Redux 7 where apparently the Redux team had attempted to use Context internally with mixed results.
Personally, I tend to use Context for data I need across components that isn't likely to change (or change much) while the user is engaged with the application, such as information about the current logged in user.
Thank you for sharing the blog post :)
Thank you for your helpful article :)
Nice article. I think redux suits larger apps more than the context API,as it helps you track what's really going on behind the scenes, especially with the redux Dev tool. Context API is much ok for rapid development of small scale apps.
Thank you :) I personally think that one should only use an external tool iff it adds more to what one has. So far I've only been working on relatively small apps, and I've mostly just been using React :) Thanks again !
Cool, I actually wrote about approach 3 (React Context) a little while back and the cool thing is that if you couple it with the
useReducer
hook, you pretty much have yourself a basic implementation of Redux!You only need Redux now if you need plugins from it or anything specific to it (or if you're not using it with React and there's no context).
Both Context and redux serve different purpose if you just have to pass props to some inner children then go ahead and use context api or there are other better ways too you need not required redux in first place.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.