When I first started working with React, Redux was pretty much the only library that was used to manage state in React. But that is not the case anymore as I wrote this post in 2024.

In this post, I will introduce several React state management libraries to you. Since I can't list every one of them here, I will only go over a few that have markedly different philosophies that are actively being used by the community. At the end, I will also share my thoughts about which one you should use for your project.

Hand drawn sketch of the word state inside a cogwheel

Redux toolkit (RTK)

Redux toolkit, commonly abbreviated to RTK, is not a state management library on its own. But it is a set of helpers that make Redux much easier to use and it is now the recommended way to use Redux.

One of the biggest complaints about Redux was the amount of boilerplate or setup code that was required to use it. RTK removes the need to write a lot of this code by automatically generating it for us.

/* A simple counter using plain Redux */
import { Provider, useSelector, useDispatch } from 'react-redux';

// Action type
const ACTIONS = {
INCREMENT: 'INCREMENT'
}

// Action creator
const increment = () => ({ type: ACTIONS.INCREMENT });

const initialState = { count: 0 };

// Reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTIONS.INCREMENT:
return { ...state, count: state.count + 1 };
default:
return state;
}
}

// Selector
const getCount = (state) => state.count;

// Assume that this is wrapped with a store using the reducer shown above
const Counter = () => {
const count = useSelector(getCount);
const dispatch = useDispatch();

return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>Add one</button>
</div>
);
}
/* A simple counter using Redux Toolkit*/
import { createSlice } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux';

// createSlice automatically generates action creators
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
// Redux Toolkit uses immer internally so state can be mutated
increment: (state) => {
state.count += 1;
}
},
selectors: {
getCount: (state) => state.count
}
});

// Assume that this is wrapped with a store using the slice shown above
const Counter = () => {
const count = useSelector(counterSlice.selectors.getCount);
const dispatch = useDispatch();

return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(counterSlice.actions.increment())}>Add one</button>
</div>
);
}

Another thing that RTK helps with is interacting with an API. Previously if you wanted to interact with an API using Redux, you had to implement it yourself using something like redux-thunk or redux-saga to handle the async operation. But RTK also consists of a helper called RTK Query that tackles interacting with an API.

/* A simple counter using RTK query that fetched the count from the API */
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; // RTK query

// createApi automatically generates a RTK slice that can interact with an API
export const counterApi = createApi({
reducerPath: 'counterApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
endpoints: (builder) => ({
getCount: builder.query({
query: () => '/count', // Makes a GET request to /api/count
}),
}),
});

// Assume that this is wrapped with a store using the slice shown above
const Counter = () => {
// counterApi.useGetCountQuery is automatically generated by createApi
const { data, error, isLoading } = counterApi.useGetCountQuery();

return (
<div>
{isLoading && (<p>Loading...</p>)}
{error && (<p>An error occurred</p>)}
<p>Count: {data?.value}</p>
</div>
);
};

Under the hood, RTK still uses Redux and the modified Flux pattern consisting of actions and reducers. So you can still write actions and reducers manually using Redux and have it work alongside RTK's generated actions and reducers.

Zustand

Zustand is a small, React hook-centric state management library. Similar to Redux, it also follows the Flux pattern but it is much more simplified. You only need to create a store with the initial state and actions and then use the generated hook to get and update the state.

/* A simple counter using Zustand */
import create from 'zustand';

const useCountStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}));

const Counter = () => {
const { count, increment } = useCountStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Add one</button>
</div>
);
}

Due to this simplicity, many in the community prefer Zustand over Redux / Redux toolkit for smaller React projects or for projects where data fetching is already handled by a dedicated data fetching library like Tanstack query.

Jotai

Jotai is a small, atomic state management library. The idea behind atomic state management is to store pieces of state in separate "atoms" and then import and use these atoms in the components that need them. Atoms can also use the value of other atoms and they automatically update when those atoms update.

/* A simple counter using Jotai */
import { atom, useAtom, useAtomValue } from 'jotai';

const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);

const Counter = () => {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom);
const increment = () => setCount(count + 1);

return (
<div>
<p>Count: {count}</p>
<p>Double Count: {doubleCount}</p>
<button onClick={increment}>Add one</button>
</div>
);
}

The idea of atomic state management was introduced by the Facebook open-source team through a library called Recoil. However, Recoil is not actively maintained anymore so Jotai is an equally good alternative that is still actively being maintained.

Valtio

Valtio is a small, proxy state management library. Proxy state management is centered around having mutable state. You wrap the state using a wrapper and then access it using a hook. You can then mutate the wrapped state and any components that read this state using the hook will automatically update. The magical part of proxy state is that components will only update when the properties they read from the state have changed. They will not update when other properties change.

/* A simple counter using plain Valtio */
import { proxy, useSnapshot } from 'valtio';

const state = proxy({ count: 0 });

const increment = () => {
state.count += 1;
};

const Counter = () => {
const snap = useSnapshot(state);
return (
<div>
<p>Count: {snap.count}</p>
<button onClick={increment}>Add one</button>
</div>
);
}

In many ways, Valtio is like a React hook-centric version of MobX.

Xstate

Xstate is a popular state machine library for Javascript that can also be used to manage the state of a React app. State machines are a concept from computer science that describes how a machine behaves by identifying the different states that it can be in and how it can transition between them. Using Xstate you can construct virtual state machines and have React render components based on the state of. You can then have React send events to the state machine to make it transition between the different states.

/* A simple counter using XState */
import { useMachine } from '@xstate/react';
import { setup, assign } from 'xstate';

const counterMachine = setup({
actions: {
// Use the built in assign action to change the context
incrementCount: assign({
count: (context) => context.count + 1
})
}
}).createMachine({
id: 'counter',
// Start in the active state
initial: 'active',
context: {
count: 0
},
states: {
active: {
on: {
// When the increment event is received, run the incrementCount action
// Since no target state is specified, remain in the active state
increment: {
actions: 'incrementCount'
}
}
}
},
});

const Counter = () => {
const [state, send] = useMachine(counterMachine);
return (
<div>
<p>Count: {state.context.count}</p>
<button onClick={() => send({ type: 'increment' })}>Add one</button>
</div>
);
}

The benefit of using state machines is that the behavior of the app can be visualized and that it is a clear way to describe complex behavior. Other state management libraries give you complete freedom to change the state however you like but state machines only allow you to transition between states in the way you specified. This makes entering an invalid, unhandled state less likely.

Which one should you use?

If you are still learning React, I recommend that you get familiar with React state and React context before diving into separate state management libraries. By sticking to these core React concepts, you will better understand the problems around managing state in React where state management libraries can help.

If you are already familiar with React state and React context, I would recommend that you use either a data-fetching library (e.g. Tanstack query, SWR) with a small state management library (e.g. Zustand, Jotai, Valtio) or Redux toolkit. The difference between these options is that data-fetching libraries and small state management libraries are easier to grasp than Redux toolkit. On the other hand, Redux toolkit is much more versatile and has a lot more content written about it. Either way, each option should serve you well.

Prabashwara Seneviratne (bash)

Written by

Prabashwara Seneviratne (bash)

Lead frontend developer