There are dozens of state management libraries for React. Choosing between them is difficult especially when you don't know how well they would work for your use case.
So in this post, I will describe several approaches for managing state in a React app and describe the pros and cons of each to help you decide.
- Using only React features
- Using a data fetching library alongside a simple state management library
- Using Redux toolkit with Redux
- Building your own state management solution
- Summary
Using only React features
The first option is not using any external libraries and using only the features that are provided by React such as the React state hooks (useState
and useReducer
) and React context to manage the state.
➕ Pros
Familiar concepts
React state hooks and React context are concepts that you learn while learning React. Most React developers are already familiar with them.
Bundle size stays the same
By not including separate libraries, the size of the Javascript bundle, that is the code that is distributed to users, will not increase. This will reduce the loading time of the app for users and improve the core web vitals score that Google considers when ranking search results.
➖ Cons
Prop drilling
While building React apps, you will inevitably encounter situations where you need the state value of a component in a different component. Moreover, you might also need to change the state value from the other component. You can pass the state value and setter function as props to the other component to achieve this result. But what if there are several components in between them?
If you are using only React state hooks, the only way to resolve this problem is to pass the value and setter function manually through each component until it reaches the other component. The React community calls this practice prop drilling and it is very tedious, especially in larger apps. It leads to "conduit" components that accept a lot of props only to pass them on to child components.
You might consider using React context to avoid prop drilling. But that causes a different set of issues.
React Context is not performant for state management
(You can read more about this issue on this post)
React context is primarily designed to share a static object with all the components that are placed within it. A big drawback of using React context for sharing state is that it is not capable of selectively updating the components that are subscribed to it. For example, if you change a single property of the state that is passed into the context, all components that use that context will re-render even if they don't use that property.
You can try to overcome this by using separate Contexts for separate pieces of state but that is also tedious.
Lacks a pattern for organizing state
React does not provide a strong pattern for organizing state out of the box. This puts the responsibility on you to organize how you read and update state and it is very easy to come up with bad patterns that make the app less performant and harder to understand.
Requires functionality to be built from scratch
The React state hooks and React context are low-level and simple. So when implementing new features, you will need to implement a lot of functionality on your own for common use cases such as fetching data from an API.
Using a data fetching library alongside a simple state management library
This option is becoming more popular in the community due to the rise of data fetching libraries (e.g. Tanstack query, SWR). These libraries focus on handling server interaction so that you don't have to do it manually. This removes a lot of the state that has to be managed manually. But there is still some globe state left (e.g. user authorization, user preferences). This is then tackled using simple state management libraries like Zustand or Jotai.
➕ Pros
More functionality out of the box
Data fetching libraries provide a lot of functionality out of the box for interacting with a server. They usually have a HTTP client set up under the hood so you only need to specify the URL and HTTP method to start talking to an API. They have built-in support for request deduplication to prevent duplicate requests from being sent to the API. They have built-in support for re-fetching the data at regular intervals or when the browser is refocused. The list goes on.
If you choose not to use a data-fetching library, you will have to implement this functionality yourself.
Simple concepts to grasp
The libraries mentioned above are easy to get started with. They consist of a few React hooks which are well documented. You can start with the basic examples and then gradually extend them to use more advanced features.
➖ Cons
Tricky to customize
Data fetching libraries behave in a specific way. You can only change its behavior to a certain extent. So in some cases, you won't be able to make it to do what you want.
For example, the data fetching libraries mentioned above don't allow operations on one API endpoint to change the data returned from another endpoint. This behavior is useful when an API has an endpoint that returns a list of items and another that changes a single item. Ideally, you should be able to change a single item and then change only that item in the list without downloading it again. But this is not possible. You can only make the library invalidate the data returned from the list and download it again.
Coordinating two libraries can be tricky
With this approach, you will be using two separate libraries and it is up to you to set up how they should work together. While doing this, it is easy to adopt patterns that are hard to maintain and inefficient. For example, you might think of storing all the data that is returned from the data fetching library to the state management library for convenience. However, doing so will increase the memory consumption of the app and also introduce unnecessary re-renders.
Using Redux toolkit with Redux
Redux is the most popular state management library for React. Before the introduction of React hooks in 2019, Redux and its Flux-inspired "Redux" pattern were pretty much the only options for state management in a React app. But it also developed a reputation for being "heavy" and needing a lot of boilerplate code.
To address these concerns, the Redux team released Redux toolkit (RTK) which reduced the amount of code that had to be written to use Redux. RTK also included RTK query which provided data fetching functionality for Redux. So compared to the previous option, RTK is a single library that provides both state management and data fetching functionality.
Redux and RTK are often chosen by teams working on larger React apps because they are the most proven. It has been used for thousands of large React apps over the years. Since RTK builds on top of Redux, it can work alongside "plain" Redux consisting of manually written reducers, actions and selectors. So many older React apps that used plain Redux have gradually switched over to RTK.
➕ Pros
The most popular option for managing state
By being the most popular option for managing state, Redux has a large community. If you face an issue, you are very likely to find something written about it online. The big community also means that many issues in the library have already been fixed.
Lots of functionality provided out of the box
RTK and the Redux library itself have a lot of functionality built-in. RTK has RTK query for data fetching, middleware for customizing Redux's behavior (e.g. Listener middleware) and built in support for thunks for handling async logic. Redux supports the Redux DevTools browser extension which makes it much easier to debug web apps that use it.
Single dependency
The previous option included two separate libraries which meant that you have to keep up with the updates and changes to both of them. A change in one library could break how you used it with the other library.
RTK is purpose-built to work with Redux. It is a single library that replaces the need to install and use Redux separately. So it is the only library that you have to keep track of with this approach. It is also smaller in size than a combination of libraries. (or at least against the combination of Tanstack query and Zustand)
Very customizable
RTK is a wrapper around Redux that makes it easier to use. But if it doesn't work the way that you would like to, you can still customize it a lot. For example, you can use the utilities provided by RTK to manually change the cached data. If this level of customization is insufficient, you can implement your own logic by using createSlice or by implementing your own reducers and actions on top of plain Redux.
➖ Cons
Steeper learning curve
The pattern used by Redux is tricky to grasp since it has a lot of terminology and moving parts (e.g. actions, actions creators, reducers, selectors, middleware). Getting familiar with all of the utilities provided by Redux Toolkit can also be a challenge because there are many of them.
Building your own state management solution
The last option, if none of the options above work for you, is to build your state management solution. Since the arrival of React hooks, this is easier since you can create a custom hook to manage state.
➕ Pros
Fully customizable
By creating a custom solution from scratch, you can have it work exactly like you would want it to. You don't have to adopt the behavior of existing libraries.
➖ Cons
Requires a lot of effort and time
While it might seem easy at a glance, managing state is a hard problem. Detecting changes in a deeply nested object is the first problem. Updating subscribers only along that path is the next problem. Issues arise when there are several components in a tree that are subscribed to the same piece of state. The list goes on.
Overcoming all of these issues takes a lot of effort and time.
Untested and undocumented
A custom solution is not going to be as tested as an open-source alternative with an active community. Documentation will also be lacking. Even if some effort is put into documentation at the start, the real challenge is keeping it up to date as the solution changes.
Summary
I hope that this post has given you a better idea of the options for managing state in a React app.
If you still can't decide, here is what I would recommend.
- If you are still learning React, then stick to using the React state hook and React context. Being familiar with them will help you understand the problems around managing state that state management libraries solve.
- If you are going to work on a personal project or a small web app, consider using a simple state management library and a data fetching library such as Tanstack query and Zustand. It should provide a lot of the functionality you need while still being easy to pick up.
- If you are going to work on a larger web app, consider using Redux Toolkit. Since it has been used for a lot of large React apps, you are more likely to find all the functionality you need as well as more content about specific edge cases.