State Management and Performance Optimizations with React Context API and Hooks
State management for React applications is a concept with a lot of alternative solutions. We have things like redux, mobx, mobx-state-tree, apollo-client and many more. They all have many different and similar approaches, learning curves, complexities and simplicities. They are very very useful for many situations. But I think, they add a lot of unnecessary layers for a lot of applications. Because, you already have a state management solution and it’s built-in to React. It’s React itself…
With good component composition, creating minimal and maintainable components, using Context API and hooks, you can easily manage your application state. It takes some time to get used to, but any other third party package would take at least the same amount of time.
That’s enough for the introduction. Now we will talk about Context API and some good approaches to improve its performance.
Enter the Counter Application
This will be the most basic type of application that you can create instantly. Now open your editor, run the terminal and just run this command;
npx create-react-app counter-app
Give some time to CRA to create your project. When it’s done, delete all the files under the
src folder and create the following files.
This is just a simple counter. Nothing fancy. Just create the files those are in the snippets and run
The result should look like this.
Click the buttons and the count will be increased or decreased. Everything works as expected.
And also, we created a simple structure with Context and hooks to prevent prop drilling. We could do it the other way. It wouldn’t be hard for a simple application like this. But when you create real world applications, it can easily go unmaintainable.
But we have a giant problem and we are not aware of it yet. Look at the console output. Everytime you click one of the buttons, each of the components those are connected to the CounterContext just re-renders. We don’t see any slowness, but if we had more complex components, some animations etc. the application might be very very slow.
Each of the components are re-rendering, because they are connected to the
CounterContext and we just update the context value everytime we click a button. Even if we just increase the
count value, it is just a property under the
value object and when we change it, its parent (
Here are some ways to overcome this.
1. Using React.memo
React.memo creates a memoized component and prevents unnecessary re-renders. So, if every prop and state of a component has the same value as the last time, it just doesn’t let the component to re-render.
The context value gets updated, but the
decrease functions still have the same memory reference (thanks to
useCallbak hook). We just need to wrap our context consuming components with
React.memo, remove their direct connection to the context, create a
Counter component which is connected to the context and pass the required values as props to each component. With this pattern, we can prevent unnecessary re-renders. Let’s update these files;
Now click the buttons a few times and look at the console output again;
Count component is re-rendering and this is exactly what we wanted. Nice!
2. Separate Contexts
Another approach is separating the context into multiple parts and providers. The only changing part of our context is the
decrease remains the same. So, if a component only wants one or both of these functions and doesn’t care about
count value, it doesn’t need to know about the update too. Now, we will divide our context into 2 parts.
CountActionsContext . Let’s see the updated files;
Now, we don’t need to use
React.memo with every context connected component. We just need to use it with
Count. Because, each time the
count state is updated,
App component re-renders and it triggers its children to re-render too. Because that the
CountActionsProvider has a
children prop, they can’t be memoized with
React.memo. Because the
children will be created from scratch each time. They will not have the same memory reference, so the shallow equality comparison of provider props will fail. By putting the
Count component in
React.memo, we ensure that even if the
Count and its children will not re-render unnecessarily.
Let’s click the buttons and look at the console output again;
We have the exact same output as the first performance optimization option. Cool!
3. Using constate
constate is a simple package to split your context value into multiple pieces. If you look at the source code of it, you will notice it’s just a few lines of code, but it is a smart design!
constate simply splits the context value with the functions you give it and creates multiple nested providers. So, if you want to use a part of the context value, you simply use the context and the hook dedicated to that part.
If you’re familiar with the
redux this feels like them. We are simply selecting some part of the context value.
Now run this command to install the package.
npm install constate
Let’s see the updated files;
Exactly the same output as the other two options.
These three options are valid and makes your application to run smoothly. You can create your own patterns while considering the requirements of your application.
Personally, I like constate the most. If I wouldn’t use it directly (it also uses TypeScript, has a medium sized community and unit tests, so why not?), I would just look at its source code and customize it to my needs. It removes a lot of effort and code from the project.
Separating context into multiple parts is a little bit verbose, while it’s the currently recommended way by the React team.
React.memo and to optimize Context may be a little bit less verbose, but it may cause prop drilling if you don’t compose components in a proper way. So, it depends…
Thanks for reading!