by author Arek Nawo
State management is one of the most important aspects of every app. The app’s state dictates what users see, how the app looks, what data is stored, and so on. Thus it’s no wonder that there are so many open-source libraries designed specifically to make state management easier and more enjoyable.
From the multitude of JavaScript UI frameworks, React is likely the one that enjoys the most vibrant ecosystem, including state management libraries. This article will go through the top 6 of those libraries and compare them to see which one is the best choice for your next React app.
Recoil
Since its announcement in early 2020, Recoil has gathered a fair bit of interest from the React community. Partially thanks to its atomic, React-inspired approach to state management, but also thanks to it coming straight from the Facebook team. Although it’s still in an “experimental” phase, Recoil is already relatively stable and feature-rich.
The core concepts to understand in Recoil are atoms and selectors. An atom is a unit of state that wraps and represents a single state property. You can think of it as an equivalent to React’s local state (useState
), but with the ability to be shared among components and created outside of them.
const isAvailableState = atom({
key: "isAvailableState", // unique, required key
default: true, // default value
});
On the other hand, selectors are pure functions that depend upon atoms or other selectors to calculate their value, recomputing when any of its dependencies are changed. They can be readable, writable, and even async, integrating nicely with React Suspense.
const statusState = selector({
key: "statusState", // unique, required key
get: ({ get }) => {
const isAvailable = get(isAvailableState); // access value of the atom
return isAvailable ? "Available" : "Unavailable";
},
});
To use atoms and selectors, you’ll need to utilize one of Recoil’s hooks, like useRecoilValue
or useRecoilState
.
// Inside React component
// state's value and state setter
const [isAvailable, setIsAvailable] = useRecoilState(isAvailableState);
// state's value only
const status = useRecoilValue(statusState);
Recoil builds upon atoms and selectors to provide advanced tools in every state management area - from coding to dev tools and testing. This approach makes Recoil easy for beginners and powerful enough for advanced users.
Jotai
Staying in the realm of atomic state management, Jotai is another library worth considering. It’s similar to Recoil, but with a smaller bundle size (3.2 kB vs. 21.1 kB), more minimalistic API, better TypeScript support, broader documentation, and no experimental label!
Apart from all the above, the most significant difference between Recoil and Jotai is probably the performance, specifically the garbage collection. If you noticed from the previous snippets, Recoil tracks its state by string keys - that’s why their required and must be unique. It’s uncomfortable, ineffective, and can lead to memory leaks.
In contrast, Jotai doesn’t require keys and depends entirely upon JavaScript’s built-in WeakMap
to track its atoms. This means the JS engine handles garbage collection automatically, allowing for optimized memory usage and thus better performance.
Jotai simplifies its core concepts even further, meaning that here, pretty much everything is an atom! Here’s how the previous Recoil snippets translate to Jotai:
const isAvailableState = atom(true);
const statusState = atom(({ get }) => {
const isAvailable = get(isAvailableState); // access value of the atom
return isAvailable ? "Available" : "Unavailable";
});
// Inside React component
// state's value and state setter
const [isAvailable, setIsAvailable] = useAtom(isAvailableState);
// ignoring state setter
const [status] = useAtom(statusState);
Jotai includes pretty much all the features you’d find in Recoil, with similarly simple integration with React Suspense and even other tools from React ecosystem like Redux or Zustand.
Redux
Speaking of which, no React state management library list would be complete without Redux. Although Redux has received a lot of criticism recently, it’s still a great, battle-proven library that can go hand-to-hand with modern solutions.
In Redux, you can change the state only by dispatching an action - an object describing what should happen. This, in turn, runs a function called a reducer, which, given an action object and previous state, returns a new state.
While the Redux model has been around for a while now, its biggest issue is the related boilerplate. Writing actions to describe every possible state change and then multiple reducers to handle those actions can lead to a lot of code, which can quickly become hard to maintain. That’s why Redux Toolkit was created.
Nowadays, Redux Toolkit is the go-to way to use Redux. It simplifies the store setup, reduces the required boilerplate, and follows the best practices by default. Additionally, it comes batteries-included with, e.g., Immer to allow for easy state changes, and Redux-Thunk to work with async logic.
const exampleSlice = createSlice({
name: "example",
initialState: {
isAvailable: true,
},
reducers: {
makeAvailable: (state) => {
state.isAvailable = true;
},
makeUnavailable(state) {
state.isAvailable = false;
},
},
});
const { makeAvailable, makeUnavailable } = exampleSlice.actions;
const exampleReducer = exampleSlice.reducer;
const store = configureStore({
reducer: { example: exampleReducer },
});
// Inside React components with React-Redux hooks
const isAvailable = useSelector((state) => state.example.isAvailable);
const dispatch = useDispatch();
dispatch(makeAvailable());
dispatch(makeUnavailable());
Rematch
Staying in the Redux world a little longer, there’s an alternative to Redux Toolkit that’s worth mentioning. It’s called Rematch and is lighter, faster, and easier to use than Redux Toolkit.
Rematch builds upon Redux core, simplifying the setup process, reducing boilerplate, and introducing simple side-effects handling with async
/await
. All that and more (like a plugin system or great TypeScript support) fits into just 1.7 kB (vs. 11.1 kB of Redux Toolkit).
At the heart of Rematch are models, which wrap state, reducers, and effects into a single entity. They enforce Redux’s best practices and make state management easier.
const countModel = {
state: 0,
reducers: {
increment(state, payload) {
return state + payload;
},
},
effects: (dispatch) => ({
async incrementAsync(payload) {
await new Promise((resolve) => setTimeout(resolve, 1000));
dispatch.count.increment(payload);
},
}),
};
The model can then be used to create a Redux store with additional Rematch functionality like shortcut action dispatchers.
const store = init({
models: {
count: countModel,
},
});
const { dispatch } = store;
dispatch({ type: "count/increment", payload: 1 });
dispatch.count.increment(1); // Action shortcut
dispatch({ type: "count/incrementAsync", payload: 1 });
dispatch.count.incrementAsync(1); // Async action shortcut
For more details on Rematch, check out this article.
Zustand
Weighting under 1 kB, Zustand is the smallest state management library on this list. With that said, don’t let the small size mislead you - Zustand’s simple, minimalistic API can achieve a lot if used properly.
Although concepts like actions or selectors exist in Zustand, the leading role is played by hooks. For example, the create
function used to create a Zustand store returns a hook for use in React components.
const useStore = create((set, get) => ({
isAvailable: true,
status: () =>
get((state) => (get().isAvailable ? "Available" : "Unavailable")),
makeAvailable: () => set((state) => ({ ...state, isAvailable: true })),
makeUnavailable: () => set((state) => ({ ...state, isAvailable: false })),
}));
// Inside React component
const { isAvailable, makeAvailable, makeUnavailable } = useStore(); // Access whole store
const status = useStore((state) => state.status); // Access selected property
On top of ease-of-use and small size, Zustand also addresses common problems in React state management, like usage in React concurrent mode or transient state updates (without causing re-renders).
Open Source Session Replay
Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder. OpenReplay is the only open-source alternative currently available.
Happy debugging, for modern frontend teams - Start monitoring your web app for free.
Hookstate
Speaking of hook-centric libraries, Hookstate definitely counts as one. This library is focused on providing an excellent development experience using hooks and functional components.
The key feature of Hookstate is, without a doubt, its API and ease of use. Whether you’re working with a global or local state - you can use the same methods with the same syntax. This means Hookstate is not only a global state management library but also useState
on steroids.
const state = createState({
isAvailable: true,
});
// Wrapped "actions"
const makeAvailable = () => state.isAvailable.set(true); // Changing state outside component
const makeUnavailable = () => state.isAvailable.set(false);
const status = () => (state.isAvailable.get() ? "Available" : "Unavailable"); // Accessing state outside component
// Inside React component
const state = useState();
const isAvailable = state.isAvailable.get(); // Access selected property
On top of that, Hookstate is very scalable thanks to its handling of nested state. With this library, you can use nested data structures with little to no performance loss thanks to mutations, scoped states, and other optimizations.
Add to all that a good extension system, TypeScript and Redux DevTools support, and Hookstate looks to be one of the most attractive solutions for fans of React hooks.
React Context
Although using a dedicated library - especially the latest and greatest one - is tempting, it’s worth noting that it’s not always necessary. In small to medium apps, adding an additional dependency, meaning additional bundle size, complexity, concepts, and more, can create more harm than good.
For such cases, React has built-in tools - namely State and Context APIs. Those can do just fine for a vast majority of apps.
The best choice is to stay with the basics until you need something more powerful. Then, explore the options - from the lightweight ones like Zustand to those with the most features and best suit for the job like Redux or Recoil.
Bottom line
So there you have it - React state management libraries you should consider for this year. Naturally, there are many more libraries like these out there, with more coming out seemingly every day. That’s why this list is likely to change within a year or even sooner. The best choice is to use what works but be aware of what’s out there, just in case something better comes around.