Prop drilling could be avoided by using React context but it is not designed for this use case and causes several performance issues where large parts of the app will re-render when the state changes. You can overcome these issues to some extent by using several context providers but it is very tedious.
A state management library addresses both problems. It provides a mechanism to easily read and update state from any component without re-rendering other parts of the app.
React does not provide a pattern for organizing state. So when working with React state hooks, you have to organize the state yourself. This can be tricky, especially if you are not familiar with React and it is easy to adopt unmaintainable and non-performant patterns.
Most state management libraries provide a pattern for working with state. By adopting that library and the pattern underlying it, you are more likely to create an app that is easier to understand and maintain.
Many state management libraries have their own pattern for managing state. Learning these patterns can be difficult since they consist of many new concepts.
Moreover, not all React developers will be familiar with the concepts of a specific library since they might not have worked with it so far. So when new team members join, it could take them longer to start work since they have to learn the concepts from scratch.
Redux is the most used state management library, partly because it is one of the oldest and the most proven. However, Redux's pattern is very verbose, meaning that a lot of code has to be written to add a new state value or operation. This led Redux and state management libraries as a whole to develop a reputation for needing a lot of boilerplate code.
However, the development experience around using Redux has changed dramatically thanks to Redux Toolkit. Redux Toolkit is a set of helpers and utilities for Redux that automatically generate the code that is needed to use Redux so that it doesn't have to be written manually. For example, this is all you need to start using a single value named count
and a single action named increment
.
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
// Redux Toolkit uses immer internally so state can be mutated here
increment: (state) => {
state.count += 1;
}
},
selectors: {
getCount: (state) => state.count
}
});
If you want to cut back even more, consider using Zustand instead.
import create from 'zustand';
const useCountStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}));
Overall, the reputation that state management libraries have gotten for needing a lot of boilerplate code is not valid anymore.
Each library you use in a React app increases the size of the Javascript "bundle" and state management libraries are no exception. A larger Javascript bundle increases the app's loading time since the browser needs to download and parse a larger Javascript file.
However, many state management libraries are relatively small compared to the React packages. The react
package and the react-dom
package add up to 136.5KB (44.4KB gzipped) while Zustand's package is 3.1KB (1.2KB gzipped). (According to bundlephobia for react@18.3.1, react-dom@18.3.1 and zustand@4.5.4) In this case, the effect of including a state management library is barely noticeable.
Admittedly, feature-packed state management libraries like Redux Toolkit are larger (39.5KB - 13.5KB gzipped). But then you should consider the amount of code that you save by using the features of the library. For example, by using Redux Toolkit's RTK query, you will not have to implement a lot of the repetitive logic around interacting with a server thus reducing the size of the Javascript bundle. So the more these features are used, the more they can offset the weight of the library.
I hope this post has helped you understand the benefits and costs of adopting a state management library.
If you still can't decide what to do, here are my 2 cents.