--- title: React State Management Libraries tags: React, State Management description: React State Management Libraries --- # React State Management Libraries ## Introduction **Why do we need to use a state management library?** A state management library is used to manage global state. **Popular state management libraries** ![](https://i.imgur.com/3sLrD3b.png) (Source: https://npmtrends.com/jotai-vs-mobx-vs-recoil-vs-redux-vs-valtio-vs-zustand) **Our examples** For our example code, we're going to demonstrate on a Next project to control the global states of a simple click counter and a todo list. Since the counter is simplier than the todo list, we're only going to show the code of the todo list state management. --- ## React Context ### Introduction **React Context** is not actually a "state management" tool. **Context** doesn't manage anything for you. Any "state management" is done by you and your own code, typically via `useState/useReducer`. **So what does Context do?** Context provides a way to pass data through the component tree without having to pass props down manually at every level. What [Sebastian Markbage (React core team architect) said about the uses for Context](https://github.com/facebook/react/issues/14110#issuecomment-448074060): > My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation. ### Codesandbox Example https://codesandbox.io/p/sandbox/context-example-gqxdpd?file=%2Fpages%2Findex.tsx ### Set Up First we create the **stores** we want to use. In `store/todoContext.tsx`: ```typescript! import { useReducer, useState, createContext } from "react"; export interface IToDo { id: string; text: string; completed: boolean; } export enum Mood { Happy = "🙂", Neutral = "😐", Worried = "😟", Angry = "😠", Sad = "😢", } const initialContextValue = { todos: [], mood: Mood.Neutral, addTodo: (text: string) => {}, deleteTodo: (id: string) => {}, editTodo: (editedTodo: IToDo) => {}, changeMood: (newMood: Mood) => {}, }; export const TodoContext = createContext<IContextValue>(initialContextValue); type Action = | { type: "ADD"; text: string } | { type: "DELETE"; id: string } | { type: "EDIT"; editedTodo: IToDo }; function todoReducer(state: IToDo[], action: Action) { switch (action.type) { case "ADD": return [ ...state, { id: String(Math.random()), text: action.text, completed: false, }, ]; case "DELETE": return state.filter((item) => item.id !== action.id); case "EDIT": return state.map((item) => { if (item.id === action.editedTodo.id) { return action.editedTodo; } return item; }); default: return state; } } export default function TodoContextProvider({ children, }: ITodoContextProviderProps) { const [todos, dispatch] = useReducer(todoReducer, []); const [mood, setMood] = useState(Mood.Neutral); function addTodo(text: string) { dispatch({ type: "ADD", text }); } function deleteTodo(id: string) { dispatch({ type: "DELETE", id }); } function editTodo(editedTodo: IToDo) { dispatch({ type: "EDIT", editedTodo }); } function changeMood(newMood: Mood) { setMood(newMood); } return ( <TodoContext.Provider value={{ todos, mood, changeMood, addTodo, deleteTodo, editTodo }} > {children} </TodoContext.Provider> ); } interface IContextValue { todos: IToDo[]; mood: Mood; addTodo: (text: string) => void; deleteTodo: (id: string) => void; editTodo: (editedTodo: IToDo) => void; changeMood: (newMood: Mood) => void; } interface ITodoContextProviderProps { children: JSX.Element; } ``` Now in our `pages/_app.tsx`, we need to wrap the **Providers** around our root components: ```typescript! import type { AppProps } from "next/app"; import Layout from "@/components/layout/Layout"; import CounterContextProvider from "@/store/counterContext"; import TodoContextProvider from "@/store/todoContext"; export default function App({ Component, pageProps }: AppProps) { return ( <CounterContextProvider> <TodoContextProvider> <Layout> <Component {...pageProps} /> </Layout> </TodoContextProvider> </CounterContextProvider> ); } ``` ### Usage In a component, we can get our **todos** and **mood** through **useContext**: ```typescript! import { useContext } from "react"; import { TodoContext } from "@/store/todoContext"; export default function Home() { const { todos, mood } = useContext(TodoContext); ... } ``` And we can also call our defined functions: ```typescript! import React, { useState, useContext } from "react"; import { IToDo, Mood, TodoContext } from "@/store/todoContext"; export default function Todos() { const { todos, mood, changeMood, addTodo } = useContext(TodoContext); const [text, setText] = useState(""); function onSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); if (!text.trim()) { return; } addTodo(text.trim()); setText(""); } ... } ``` ### Dev Tools [React Developer Tools Chrome extension](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) allows you to inspect React state changes, which includes Context states. ![](https://i.imgur.com/sfnahl3.png) --- ## Redux ### Introduction **Redux** is currently the most popular React state management library. It also includes state traceability, middleware, and **improved component rendering performance**. Before, there have been a lot of complaints about the complexity and boilerplate required for Redux, but this has changed with the modern Redux by using [**Redux Toolkit**](https://redux-toolkit.js.org/). - With Tookit, set up is really simple - Toolkit automatically uses [Immer](https://redux-toolkit.js.org/usage/immer-reducers) internally so you can write simpler immutable update logic using "mutating" syntax. Redux maintainer Mark "acemarke" Erikson's opinion on **Context vs Redux**: > My personal opinion is that if you get past 2-3 state-related contexts in an application, you're re-inventing a weaker version of React-Redux and should just switch to using Redux ### Installation `npm install @reduxjs/toolkit` `npm install react-redux` ### Codesandbox Example https://codesandbox.io/p/sandbox/redux-example-p2cduw?file=%2Fpages%2Findex.tsx ### Set Up First we create our **slices**. In `slices/todoSlice.tsx`: ```typescript! import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; export interface IToDo { id: string; text: string; completed: boolean; } export interface ITodoState { todos: IToDo[]; mood: Mood; } export enum Mood { Happy = "🙂", Neutral = "😐", Worried = "😟", Angry = "😠", Sad = "😢", } const initialState: ITodoState = { todos: [], mood: Mood.Neutral, }; export const todoSlice = createSlice({ name: "todo", initialState, reducers: { addTodo: (state, action: PayloadAction<string>) => { state.todos.push({ id: String(Math.random()), text: action.payload, completed: false, }); }, deleteTodo: (state, action: PayloadAction<string>) => { state.todos = state.todos.filter((item) => item.id !== action.payload); }, editTodo: (state, action: PayloadAction<IToDo>) => { const index = state.todos.findIndex( (item) => item.id === action.payload.id ); if (index > -1) { state.todos[index] = action.payload; } }, changeMood: (state, action: PayloadAction<Mood>) => { state.mood = action.payload; }, }, }); // Action creators are generated for each case reducer function export const { addTodo, deleteTodo, editTodo, changeMood } = todoSlice.actions; export default todoSlice.reducer; ``` Create a Redux Store in `store.ts`: ```typescript! import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "./slices/counterSlice"; import todoReducer from "./slices/todoSlice"; export const store = configureStore({ reducer: { counter: counterReducer, todo: todoReducer, }, }); // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType<typeof store.getState>; // Inferred type export type AppDispatch = typeof store.dispatch; ``` Now in our `pages/_app.tsx`, we need to wrap the **Provider** around our root components: ```typescript! import type { AppProps } from "next/app"; import { store } from "../store"; import { Provider } from "react-redux"; import Layout from "@/components/layout/Layout"; export default function App({ Component, pageProps }: AppProps) { return ( <Provider store={store}> <Layout> <Component {...pageProps} /> </Layout> </Provider> ); } ``` ### Usage In a component, we can get our **todos** and **mood** through **useSelector**: ```typescript! import { useSelector } from "react-redux"; import type { RootState } from "@/store"; export default function Home() { const { todos, mood } = useSelector((state: RootState) => state.todo); ... } ``` We can call our **reducers** by using **dispatch**: ```typescript! import React, { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { IToDo, Mood, addTodo, changeMood } from "@/slices/todoSlice"; export default function Todos() { const dispatch = useDispatch(); const { todos, mood } = useSelector((state: RootState) => state.todo); const [text, setText] = useState(""); function onSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); if (!text.trim()) { return; } dispatch(addTodo(text.trim())); setText(""); } ... } ``` ### Dev Tools [Redux DevTools Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en) allows you to inspect Redux state changes. ![](https://i.imgur.com/WGRR7og.png) --- ## Zustand ### Introduction [Zustand](https://github.com/pmndrs/zustand) is a small, fast and scalable bearbones state-management solution. - No need to wrap your app in context providers. - The `set` function [merges your state](https://github.com/pmndrs/zustand/blob/main/docs/guides/immutable-state-and-merging.md) so we can skip the `...state` part. ### Installation `npm install zustand` ### Codesandbox Example https://codesandbox.io/p/sandbox/zustand-example-x4g48u?file=%2Fpages%2Findex.tsx ### Set Up First we create our **stores**. In `store/todoStore.tsx`: ```typescript! import { create } from "zustand"; import { devtools } from "zustand/middleware"; export interface IToDo { id: string; text: string; completed: boolean; } export enum Mood { Happy = "🙂", Neutral = "😐", Worried = "😟", Angry = "😠", Sad = "😢", } export const useTodoStore = create<ITodoState>()( devtools( (set) => ({ todos: [], mood: Mood.Neutral, // ⬇️ separate "namespace" for actions actions: { addTodo: (text: string) => set((state) => { const newTodo = { id: String(Math.random()), text: text, completed: false, }; return { todos: [...state.todos, newTodo] }; }), deleteTodo: (id: string) => set((state) => { const newTodos = state.todos.filter((item) => item.id !== id); return { todos: newTodos }; }), editTodo: (editedTodo: IToDo) => set((state) => { const newTodos = state.todos.map((item) => { if (item.id === editedTodo.id) { return editedTodo; } return item; }); return { todos: newTodos }; }), changeMood: (newMood: Mood) => { set((state) => { return { mood: newMood }; }); }, }, }), { name: "todoStore" } ) ); // one selector for all our actions export const useTodoActions = () => useTodoStore((state) => state.actions); interface ITodoState { todos: IToDo[]; mood: Mood; actions: { addTodo: (text: string) => void; deleteTodo: (id: string) => void; editTodo: (editedTodo: IToDo) => void; changeMood: (newMood: Mood) => void; }; } ``` ### Usage In a component, we can get our **todos** and **mood** through **useTodoStore**: - **Note:** Reason to pass "shallow" when selecting multiple states is explained [here](https://tkdodo.eu/blog/working-with-zustand#prefer-atomic-selectors). ```typescript! import { shallow } from "zustand/shallow"; import { useTodoStore } from "@/store/todoStore"; export default function Home() { // you can get the states individually like this const count = useCounterStore((state) => state.count); // ⬇️ need to pass "shallow" here when selecting multiple states for optimization const { todos, mood } = useTodoStore( (state) => ({ todos: state.todos, mood: state.mood }), shallow ); ... } ``` We can get our **actions** by using **useTodoActions**: ```typescript! import React, { useState } from "react"; import { useTodoActions } from "@/store/todoStore"; export default function Todos() { // actions never change so we can subscribe to all of them const { addTodo, changeMood } = useTodoActions(); const [text, setText] = useState(""); function onSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); if (!text.trim()) { return; } addTodo(text.trim()); setText(""); } ... } ``` ### Dev Tools You can also use [Redux DevTools Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en) with Zustand to inspect state changes. ![](https://i.imgur.com/cOt2Jlp.png) --- ## Comparison | | Context | Redux Toolkit| Zustand | Valtio | | ----------------| --------------| -------------| -------- | -------| | Documentation | Good | Good | Lacking | Lacking| | Community | Large | Large | Medium | Small | | Usability | Simple | Simple | Simple | Simple | | Performance | Not Optimized | Optimized | Optimized| Optimized| | Maintainability | Good | Good | Medium | Medium | --- ## Conclusion Context and Redux are both more suitable for large scale apps, but Redux has better optimization, which makes it better for frequent state changes. Redux actually uses Context under the hood, and adds a lot of optimization, solving problems that Context has with too much re-rendering. Redux prevents unnecessary re-renders. Additionally, Redux has a well-established set of debugging tools and middleware, which makes it easier to trace the flow of data. --- ## References ### React Context - https://react.dev/reference/react/createContext - https://react.dev/reference/react/useContext ### Redux Toolkit - https://redux-toolkit.js.org/tutorials/quick-start - https://redux-toolkit.js.org/usage/immer-reducers ### Zustand - https://github.com/pmndrs/zustand - https://tkdodo.eu/blog/working-with-zustand ### Valtio - https://valtio.pmnd.rs/docs/introduction/getting-started - https://marmelab.com/blog/2022/06/23/proxy-state-with-valtio.html ### State management comparisons - https://blog.isquaredsoftware.com/2021/01/context-redux-differences/ - https://www.matillion.com/resources/blog/react-state-management-libraries-which-one - https://www.toptal.com/react/react-state-management-tools-enterprise