A useState, Context and Redux Comparison
Whether is through user input by clicking around the website, API fetches and responses, loading pages or any other type of event, data tends to change constantly in web development. Managing this can become increasingly challenging to manage as an application grows, which means having more moving parts and trying to keep them all in-sync. Working with frameworks like React makes this task easier for the developers as it uses different built-in options and libraries to handle these data changes and their propagation throughout the application depending each situation.
In this article we will briefly explain what useState, Context and Redux are, what is their main purpose and how are they typically used, as well as compare the different scenarios where it’s needed to handle data changes that affect multiple components within a React application.
useState
useState is a React Hook that allows us to have a state variable available which will re-render the view accordingly. This is the simplest form of state management in React and works great for simple value changes, such as a Boolean, a Number or a String and even Objects, Arrays and Functions as well (although this would require a few extra steps when updating the values). Below you can see a very simple implementation of how to read and update the state of a value using useState with a very typical counter component:
import { useState } from 'react';
function MyCounter() {
const [counter, setCounter] = useState(0);
return (
<div>
<p>The count is: {counter}</p>
<button onClick={() => setCounter(counter + 1)}>Add one more</button>
</div>
);
}
We configured the useState with a current state readable value (in this case counter
) and a set function (in this case setCounter
), which is the only way to update the state. This gives us more control to know exactly when and where is the value being modified. The last configuration is the initial value, which is the one in parenthesis, in this case a zero. As you can see in the code example, when the user clicks the button, the counter state is changing to whatever the new value is being sent as a parameter. In this example, we are reading the current counter state, which initially is zero, and adding one to it, so on the first click, we are sending 1
as a parameter, setting the new state to it.
Now that we get the basics of state management within the same component, let’s talk about communicating this data changes within multiple components. The state can be used as a prop to pass down to a children component, and by doing so, you make sure the different components at play remain synced on the same value state. Let’s imagine there is a child component which has its own self-contained structure and styling and it’s only responsibility is to display the current counter value. As you can see in this example, although the child component has access to the current state of counter
it can’t change its value since the set function, setCounter
, is currently only available in the parent component.
import { useState } from 'react';
function ChildCounterDisplay({ countToDisplay }) {
return <p>The count is: {countToDisplay}</p>
}
function MyCounter() {
const [counter, setCounter] = useState(0);
return (
<div>
<ChildCounterDisplay countToDisplay={counter} />
<button onClick={() => setCounter(counter + 1)}>Add one more</button>
</div>
);
}
This is perfect when having a couple of values to update and the scope stays relatively small, either from a parent to child component or a couple of components down where there’s not a complex parent-children communication going on. But sometimes, you need to pass down data from one parent component through multiple layers of child components down. When the data needs to reach multiple components at different levels of the tree, using a Context might be a better solution for propagating this changes.
Context
Let’s say you are working on a website that sells online courses. In this project, you have a Page and inside it there is a Container component, within it, you have a Section component that has a title, a description and a Carousel component, which consists of a set of Card components, and each card has an image, some text and a Button component. These buttons are links to some courses that are behind a paywall, so need to know if the user is authenticated or not so we can either enable these buttons, or disable the button and show a different label. We get the user authentication information that comes from an API response that should come only from the Page component and in order for that to reach the button of the card by passing down props from parent to child, it would need to go from the Container to the Section, from that to the Carousel , from that to the Card and finally to the Button. This is approach simply not sustainable.
Context, as it name implies, provides a shared context within components. This means that instead of passing data down the tree from parent to child components, whatever value is set in this context will be able to be accessed by any component within the tree of the element that is wrapped by the Context Provider. If you take a look at the code below, let’s say we have a similar example to the one I described previously, trying to keep it as simple as possible, let’s try to implement a Context with a useState state variable.
// This would be a separate file exclusive for the AuthContext
import { createContext } from 'react';
// First create the context using createContext with a default value and export it
export const AuthContext = createContext({ user_authenticated: false });
//...
// In the Page component, we import AuthContext
import { AuthContext } from './auth-context';
function Page() {
//useState handles the auth state for the Context
const [auth, setAuth] = useState({ user_authenticated: false });
//...
// We update the state with the API response after the user logs in
setAuthentication({user_authenticated: true})
/...
// We wrap our Main Container component with the Context Provider
return (
<AuthContext.Provider value={auth}>
<MainContainer />
</AuthContext.Provider>
);
}
//...
// This is the Button component used inside the Card component
import { AuthContext } from './auth-context';
function Button() {
// We use the imported AuthContext we created earlier
const auth = useContext(AuthContext);
// Now we have access to the context and we can handle each auth case
if (auth.user_authenticated) {
return <button className="button" onClick={goToCourse}>Learn More</button>
else {
return <button className="button disabled">Not Available</button>;
}
}
Even though it requires some extra configuration, using Context allows you to have a control center for your data and an easier way to communicate the state of this data within all of your components. Expanding on the example above, let’s imagine a design change comes in and now this Button component is moved elsewhere in the Page. Because the Button is still part of the Page node tree, it will continue to have access to its Context, therefore, it will continue to work as it was and adapt to whether the user is authenticated or not. If we had followed the parent-child communication approach, there would’ve been a lot of rework to do.
Redux
Redux is a library created by React for global state management which follows a pattern that allows to handle the data updates easily, consistently and separately from the rest of the application. As they put it in their own website, it allows the developers to control exactly when, where, why and how did the state changed. Any component within an application that uses Redux could have access to its current state and makes it easier to keep a complex solution completely in-sync. It offers tools to visualize the state changes at any given time in the browser and it’s not exclusive for React and it is widely used with other frameworks. Redux is quite complex and going into detail on code implementation would be outside of the scope of this article, so this will only be a high-level overview to understand it as a concept and compare it to useState and Context.
In Redux, the divide-and-conquer mentality is embraced and the big task of global state management gets broken down into smaller and simpler tasks. The process looks like a bit a machine, where each piece of its cyclical system has basically one responsibility and whenever it has fulfilled its duty, it is the turn of the next piece. There are multiple interpretations on how this pattern should be visualized, but there are basically 4 main pieces at play:
- UI
- Sometimes called View or Component as well, it is what the users see on a website or application, what they interact with where an action could get triggered from. Let’s think of it as a customer at a restaurant that wants to order some food.
- Actions
- An action is like a middle-man, simply taking the trigger from the UI and dispatches the action to the store, nothing more. This would be the waiter taking the order from the customer and delivering it to the kitchen.
- Store
- This is where it can get a bit confusing, since the store handles multiple aspects. This is where the state lives and where any update to it happens. As soon as the dispatch from the action comes, the store sends the current state and the function that needs to be made to the reducer, which is basically within the store itself, or at least in a closed loop. Once the reducer is done processing the new value, the store takes it, updates the state, and announces the change to the UI, which then re-renders accordingly to reflect the updated state. Let’s say this would be the main chef of the kitchen, who is not there to cook but to manage the entire kitchen. In this restaurant, once the food is ready, the main chef personally delivers the food to the customer.
- Reducer
- The reducer is where the logic happens. It’s where by taking the dispatched action, it applies the needed logic to update the current state into the new state. For example, back to our counter component from before, the reducer would be the function that takes the current state of the counter and sums one to it. In this case, the reducer doesn’t update the state directly, it only gets that result processed and passes it back to the store. In the restaurant, this would be one of the chef that cooks the food and gets the dish ready to be served to the customer by the main chef.
You can refer to this diagram below for better visualization of the data flow between these 4 steps in the Redux state management process. In case you are interested in diving deep on it, I suggest you check their official documentation at https://redux.js.org/introduction/getting-started where it goes into detail on what Redux is and how to start implementing it in your project.
Final Comparison Thoughts
Compared to Redux, Context is not a stage management pattern, but it can use a useState state variable from the component where the Context Provider is implemented and make this state accessible to the components within that tree without the need to go down each level, which is very easy to implement. One disadvantage of using Context is that a component consuming it will be forced to re-render every time the context gets updated, even though this component is not using the part of the Context that actually got updated. This might not be ideal on implementations using large objects as Context containing multiple states meant for different components.
In contrast, Redux requires a somewhat complex setup on different files across the code base and has a steeper learning curve, but it makes global state management easier, especially on large-scale projects where it’s easier to get lost on the data flow across multiple components. Redux centralizes the logic for handling the state, having basically one source of truth, making debugging easier. However, keep in mind that this does mean that every other integration, for instance with an API, will have to be fitted to this pattern and it could take more time compared to not using it.
Having said that, this comparison is not exactly a matter of project size. Whether your focus is to have an easier time passing the data within components or if it’s to implement better organization for the state management on your project, these are different tools that do different things but can serve similar purposes when it comes to managing data changes and communicating those throughout your application to keep all your components synced at all times. It all depends on the needs of the project and the opportunity to explore solutions for them.