Zustand Full documentation content. Logo Zustand A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks. It isn't boilerplatey or opinionated, but has enough convention to be explicit and flux-like. Don't disregard it because it's cute, it has claws! Lots of time was spent to deal with common pitfalls, like the dreaded [zombie child problem], [React concurrency], and [context loss] between mixed renderers. It may be the one state manager in the React space that gets all of these right. You can try a live demo [here](https://codesandbox.io/s/dazzling-moon-itop4). [zombie child problem]: https://react-redux.js.org/api/hooks#stale-props-and-zombie-children [react concurrency]: https://github.com/bvaughn/rfcs/blob/useMutableSource/text/0000-use-mutable-source.md [context loss]: https://github.com/facebook/react/issues/13332 ## Installation Zustand is available as a package on NPM for use: ```bash # NPM npm install zustand # Or, use any package manager of your choice. ``` ## First create a store Your store is a hook! You can put anything in it: primitives, objects, functions. The `set` function _merges_ state. ```js import { create } from 'zustand' const useBear = create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), updateBears: (newBears) => set({ bears: newBears }), })) ``` ## Then bind your components, and that's it! You can use the hook anywhere, without the need of providers. Select your state and the consuming component will re-render when that state changes. ```jsx function BearCounter() { const bears = useBear((state) => state.bears) return

{bears} bears around here...

} function Controls() { const increasePopulation = useBear((state) => state.increasePopulation) return } ``` ]]>
void decrement: (qty: number) => void } const useCountStore = create((set) => ({ count: 0, increment: (qty: number) => set((state) => ({ count: state.count + qty })), decrement: (qty: number) => set((state) => ({ count: state.count - qty })), })) ``` ```ts import { create } from 'zustand' type State = { count: number } type Action = { type: 'increment' | 'decrement' qty: number } type Actions = { dispatch: (action: Action) => void } const countReducer = (state: State, action: Action) => { switch (action.type) { case 'increment': return { count: state.count + action.qty } case 'decrement': return { count: state.count - action.qty } default: return state } } const useCountStore = create((set) => ({ count: 0, dispatch: (action: Action) => set((state) => countReducer(state, action)), })) ``` **Redux** ```ts import { createStore } from 'redux' import { useSelector, useDispatch } from 'react-redux' type State = { count: number } type Action = { type: 'increment' | 'decrement' qty: number } const countReducer = (state: State, action: Action) => { switch (action.type) { case 'increment': return { count: state.count + action.qty } case 'decrement': return { count: state.count - action.qty } default: return state } } const countStore = createStore(countReducer) ``` ```ts import { createSlice, configureStore } from '@reduxjs/toolkit' const countSlice = createSlice({ name: 'count', initialState: { value: 0 }, reducers: { incremented: (state, qty: number) => { // Redux Toolkit does not mutate the state, it uses the Immer library // behind scenes, allowing us to have something called "draft state". state.value += qty }, decremented: (state, qty: number) => { state.value -= qty }, }, }) const countStore = configureStore({ reducer: countSlice.reducer }) ``` ### Render Optimization (vs Redux) When it comes to render optimizations within your app, there are no major differences in approach between Zustand and Redux. In both libraries it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { increment: (qty: number) => void decrement: (qty: number) => void } const useCountStore = create((set) => ({ count: 0, increment: (qty: number) => set((state) => ({ count: state.count + qty })), decrement: (qty: number) => set((state) => ({ count: state.count - qty })), })) const Component = () => { const count = useCountStore((state) => state.count) const increment = useCountStore((state) => state.increment) const decrement = useCountStore((state) => state.decrement) // ... } ``` **Redux** ```ts import { createStore } from 'redux' import { useSelector, useDispatch } from 'react-redux' type State = { count: number } type Action = { type: 'increment' | 'decrement' qty: number } const countReducer = (state: State, action: Action) => { switch (action.type) { case 'increment': return { count: state.count + action.qty } case 'decrement': return { count: state.count - action.qty } default: return state } } const countStore = createStore(countReducer) const Component = () => { const count = useSelector((state) => state.count) const dispatch = useDispatch() // ... } ``` ```ts import { useSelector } from 'react-redux' import type { TypedUseSelectorHook } from 'react-redux' import { createSlice, configureStore } from '@reduxjs/toolkit' const countSlice = createSlice({ name: 'count', initialState: { value: 0 }, reducers: { incremented: (state, qty: number) => { // Redux Toolkit does not mutate the state, it uses the Immer library // behind scenes, allowing us to have something called "draft state". state.value += qty }, decremented: (state, qty: number) => { state.value -= qty }, }, }) const countStore = configureStore({ reducer: countSlice.reducer }) const useAppSelector: TypedUseSelectorHook = useSelector const useAppDispatch: () => typeof countStore.dispatch = useDispatch const Component = () => { const count = useAppSelector((state) => state.count.value) const dispatch = useAppDispatch() // ... } ``` ## Valtio ### State Model (vs Valtio) Zustand and Valtio approach state management in a fundamentally different way. Zustand is based on the **immutable** state model, while Valtio is based on the **mutable** state model. **Zustand** ```ts import { create } from 'zustand' type State = { obj: { count: number } } const store = create(() => ({ obj: { count: 0 } })) store.setState((prev) => ({ obj: { count: prev.obj.count + 1 } })) ``` **Valtio** ```ts import { proxy } from 'valtio' const state = proxy({ obj: { count: 0 } }) state.obj.count += 1 ``` ### Render Optimization (vs Valtio) The other difference between Zustand and Valtio is Valtio makes render optimizations through property access. However, with Zustand, it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } const useCountStore = create(() => ({ count: 0, })) const Component = () => { const count = useCountStore((state) => state.count) // ... } ``` **Valtio** ```ts import { proxy, useSnapshot } from 'valtio' const state = proxy({ count: 0, }) const Component = () => { const { count } = useSnapshot(state) // ... } ``` ## Jotai ### State Model (vs Jotai) There is one major difference between Zustand and Jotai. Zustand is a single store, while Jotai consists of primitive atoms that can be composed together. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { updateCount: ( countCallback: (count: State['count']) => State['count'], ) => void } const useCountStore = create((set) => ({ count: 0, updateCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) ``` **Jotai** ```ts import { atom } from 'jotai' const countAtom = atom(0) ``` ### Render Optimization (vs Jotai) Jotai achieves render optimizations through atom dependency. However, with Zustand it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { updateCount: ( countCallback: (count: State['count']) => State['count'], ) => void } const useCountStore = create((set) => ({ count: 0, updateCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) const Component = () => { const count = useCountStore((state) => state.count) const updateCount = useCountStore((state) => state.updateCount) // ... } ``` **Jotai** ```ts import { atom, useAtom } from 'jotai' const countAtom = atom(0) const Component = () => { const [count, updateCount] = useAtom(countAtom) // ... } ``` ## Recoil ### State Model (vs Recoil) The difference between Zustand and Recoil is similar to that between Zustand and Jotai. Recoil depends on atom string keys instead of atom object referential identities. Additionally, Recoil needs to wrap your app in a context provider. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { setCount: (countCallback: (count: State['count']) => State['count']) => void } const useCountStore = create((set) => ({ count: 0, setCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) ``` **Recoil** ```ts import { atom } from 'recoil' const count = atom({ key: 'count', default: 0, }) ``` ### Render Optimization (vs Recoil) Similar to previous optimization comparisons, Recoil makes render optimizations through atom dependency. Whereas with Zustand, it is recommended that you manually apply render optimizations by using selectors. **Zustand** ```ts import { create } from 'zustand' type State = { count: number } type Actions = { setCount: (countCallback: (count: State['count']) => State['count']) => void } const useCountStore = create((set) => ({ count: 0, setCount: (countCallback) => set((state) => ({ count: countCallback(state.count) })), })) const Component = () => { const count = useCountStore((state) => state.count) const setCount = useCountStore((state) => state.setCount) // ... } ``` **Recoil** ```ts import { atom, useRecoilState } from 'recoil' const countAtom = atom({ key: 'count', default: 0, }) const Component = () => { const [count, setCount] = useRecoilState(countAtom) // ... } ``` ## Npm Downloads Trend - [Npm Downloads Trend of State Management Libraries for React](https://npm-stat.com/charts.html?package=zustand&package=jotai&package=valtio&package=%40reduxjs%2Ftoolkit&package=recoil) ]]> [!NOTE] > This tutorial is crafted for those who learn best through hands-on experience and want to swiftly > create something tangible. It draws inspiration from React's tic-tac-toe tutorial. The tutorial is divided into several sections: - Setup for the tutorial will give you a starting point to follow the tutorial. - Overview will teach you the fundamentals of React: components, props, and state. - Completing the game will teach you the most common techniques in React development. - Adding time travel will give you a deeper insight into the unique strengths of React. ### What are you building? In this tutorial, you'll build an interactive tic-tac-toe game with React and Zustand. You can see what it will look like when you're finished here: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine( { history: [Array(9).fill(null)], currentMove: 0, }, (set, get) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setCurrentMove: (nextCurrentMove) => { set((state) => ({ currentMove: typeof nextCurrentMove === 'function' ? nextCurrentMove(state.currentMove) : nextCurrentMove, })) }, } }, ), ) function Square({ value, onSquareClick }) { return ( ) } function Board({ xIsNext, squares, onPlay }) { const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player onPlay(nextSquares) } return ( <>
{status}
{squares.map((_, i) => ( handleClick(i)} /> ))}
) } export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const currentMove = useGameStore((state) => state.currentMove) const setCurrentMove = useGameStore((state) => state.setCurrentMove) const xIsNext = currentMove % 2 === 0 const currentSquares = history[currentMove] function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares] setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) } function jumpTo(nextMove) { setCurrentMove(nextMove) } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` ### Building the board Let's start by creating the `Square` component, which will be a building block for our `Board` component. This component will represent each square in our game. The `Square` component should take `value` and `onSquareClick` as props. It should return a ` ) } ``` Let's move on to creating the Board component, which will consist of 9 squares arranged in a grid. This component will serve as the main playing area for our game. The `Board` component should return a `
` element styled as a grid. The grid layout is achieved using CSS Grid, with three columns and three rows, each taking up an equal fraction of the available space. The overall size of the grid is determined by the width and height properties, ensuring that it is square-shaped and appropriately sized. Inside the grid, we place nine Square components, each with a value prop representing its position. These Square components will eventually hold the game symbols (`'X'` or `'O'`) and handle user interactions. Here's the code for the `Board` component: ```jsx export default function Board() { return (
) } ``` This Board component sets up the basic structure for our game board by arranging nine squares in a 3x3 grid. It positions the squares neatly, providing a foundation for adding more features and handling player interactions in the future. ### Lifting state up Each `Square` component could maintain a part of the game's state. To check for a winner in a tic-tac-toe game, the `Board` component would need to somehow know the state of each of the 9 `Square` components. How would you approach that? At first, you might guess that the `Board` component needs to ask each `Square` component for that `Square`'s component state. Although this approach is technically possible in React, we discourage it because the code becomes difficult to understand, susceptible to bugs, and hard to refactor. Instead, the best approach is to store the game's state in the parent `Board` component instead of in each `Square` component. The `Board` component can tell each `Square` component what to display by passing a prop, like you did when you passed a number to each `Square` component. > [!IMPORTANT] > To collect data from multiple children, or to have two or more child components > communicate with each other, declare the shared state in their parent component instead. The > parent component can pass that state back down to the children via props. This keeps the child > components in sync with each other and with their parent. Let's take this opportunity to try it out. Edit the `Board` component so that it declares a state variable named squares that defaults to an array of 9 nulls corresponding to the 9 squares: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine({ squares: Array(9).fill(null) }, (set) => { return { setSquares: (nextSquares) => { set((state) => ({ squares: typeof nextSquares === 'function' ? nextSquares(state.squares) : nextSquares, })) }, } }), ) export default function Board() { const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) return (
{squares.map((square, squareIndex) => ( ))}
) } ``` `Array(9).fill(null)` creates an array with nine elements and sets each of them to `null`. The `useGameStore` declares a `squares` state that's initially set to that array. Each entry in the array corresponds to the value of a square. When you fill the board in later, the squares array will look like this: ```js const squares = ['O', null, 'X', 'X', 'X', 'O', 'O', null, null] ``` Each Square will now receive a `value` prop that will either be `'X'`, `'O'`, or `null` for empty squares. Next, you need to change what happens when a `Square` component is clicked. The `Board` component now maintains which squares are filled. You'll need to create a way for the `Square` component to update the `Board`'s component state. Since state is private to a component that defines it, you cannot update the `Board`'s component state directly from `Square` component. Instead, you'll pass down a function from the Board component to the `Square` component, and you'll have `Square` component call that function when a square is clicked. You'll start with the function that the `Square` component will call when it is clicked. You'll call that function `onSquareClick`: Now you'll connect the `onSquareClick` prop to a function in the `Board` component that you'll name `handleClick`. To connect `onSquareClick` to `handleClick` you'll pass an inline function to the `onSquareClick` prop of the first Square component: ```jsx handleClick(i)} /> ``` Lastly, you will define the `handleClick` function inside the `Board` component to update the squares array holding your board's state. The `handleClick` function should take the index of the square to update and create a copy of the `squares` array (`nextSquares`). Then, `handleClick` updates the `nextSquares` array by adding `X` to the square at the specified index (`i`) if is not already filled. ```jsx {5-10,27} export default function Board() { const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) function handleClick(i) { if (squares[i]) return const nextSquares = squares.slice() nextSquares[i] = 'X' setSquares(nextSquares) } return (
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` > [!IMPORTANT] > Note how in `handleClick` function, you call `.slice()` to create a copy of the squares array > instead of modifying the existing array. ### Taking turns It's now time to fix a major defect in this tic-tac-toe game: the `'O'`s cannot be used on the board. You'll set the first move to be `'X'` by default. Let's keep track of this by adding another piece of state to the `useGameStore` hook: ```jsx {2,12-18} const useGameStore = create( combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { return { setSquares: (nextSquares) => { set((state) => ({ squares: typeof nextSquares === 'function' ? nextSquares(state.squares) : nextSquares, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) ``` Each time a player moves, `xIsNext` (a boolean) will be flipped to determine which player goes next and the game's state will be saved. You'll update the Board's `handleClick` function to flip the value of `xIsNext`: ```jsx {2-3,6,11} export default function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const player = xIsNext ? 'X' : 'O' function handleClick(i) { if (squares[i]) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return (
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` ### Declaring a winner or draw Now that the players can take turns, you'll want to show when the game is won or drawn and there are no more turns to make. To do this you'll add three helper functions. The first helper function called `calculateWinner` that takes an array of 9 squares, checks for a winner and returns `'X'`, `'O'`, or `null` as appropriate. The second helper function called `calculateTurns` that takes the same array, checks for remaining turns by filtering out only `null` items, and returns the count of them. The last helper called `calculateStatus` that takes the remaining turns, the winner, and the current player (`'X' or 'O'`): ```js function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` You will use the result of `calculateWinner(squares)` in the Board component's `handleClick` function to check if a player has won. You can perform this check at the same time you check if a user has clicked a square that already has a `'X'` or and `'O'`. We'd like to return early in both cases: ```js {2} function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } ``` To let the players know when the game is over, you can display text such as `'Winner: X'` or `'Winner: O'`. To do that you'll add a `status` section to the `Board` component. The status will display the winner or draw if the game is over and if the game is ongoing you'll display which player's turn is next: ```jsx {6-7,9,21} export default function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` Congratulations! You now have a working tic-tac-toe game. And you've just learned the basics of React and Zustand too. So you are the real winner here. Here is what the code should look like: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { return { setSquares: (nextSquares) => { set((state) => ({ squares: typeof nextSquares === 'function' ? nextSquares(state.squares) : nextSquares, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) function Square({ value, onSquareClick }) { return ( ) } export default function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` ### Adding time travel As a final exercise, let's make it possible to “go back in time” and revisit previous moves in the game. If you had directly modified the squares array, implementing this time-travel feature would be very difficult. However, since you used `slice()` to create a new copy of the squares array after every move, treating it as immutable, you can store every past version of the squares array and navigate between them. You'll keep track of these past squares arrays in a new state variable called `history`. This `history` array will store all board states, from the first move to the latest one, and will look something like this: ```js const history = [ // First move [null, null, null, null, null, null, null, null, null], // Second move ['X', null, null, null, null, null, null, null, null], // Third move ['X', 'O', null, null, null, null, null, null, null], // and so on... ] ``` This approach allows you to easily navigate between different game states and implement the time-travel feature. ### Lifting state up, again Next, you will create a new top-level component called `Game` to display a list of past moves. This is where you will store the `history` state that contains the entire game history. By placing the `history` state in the `Game` component, you can remove the `squares` state from the `Board` component. You will now lift the state up from the `Board` component to the top-level `Game` component. This change allows the `Game` component to have full control over the `Board`'s component data and instruct the `Board` component to render previous turns from the `history`. First, add a `Game` component with `export default` and remove it from `Board` component. Here is what the code should look like: ```jsx {1,44-61} function Board() { const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const squares = useGameStore((state) => state.squares) const setSquares = useGameStore((state) => state.setSquares) const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player setSquares(nextSquares) setXIsNext(!xIsNext) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } export default function Game() { return (
    {/* TODO */}
) } ``` Add some state to the `useGameStore` hook to track the history of moves: ```js {2,4-11} const useGameStore = create( combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) ``` Notice how `[Array(9).fill(null)]` creates an array with a single item, which is itself an array of 9 null values. To render the squares for the current move, you'll need to read the most recent squares array from the `history` state. You don't need an extra state for this because you already have enough information to calculate it during rendering: ```jsx {2-6} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] return (
    {/*TODO*/}
) } ``` Next, create a `handlePlay` function inside the `Game` component that will be called by the `Board` component to update the game. Pass `xIsNext`, `currentSquares` and `handlePlay` as props to the `Board` component: ```jsx {8-10,21} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] function handlePlay(nextSquares) { // TODO } return (
    {/*TODO*/}
) } ``` Let's make the `Board` component fully controlled by the props it receives. To do this, we'll modify the `Board` component to accept three props: `xIsNext`, `squares`, and a new `onPlay` function that the `Board` component can call with the updated squares array when a player makes a move. ```jsx {1} function Board({ xIsNext, squares, onPlay }) { const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player onPlay(nextSquares) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } ``` The `Board` component is now fully controlled by the props passed to it by the `Game` component. To get the game working again, you need to implement the `handlePlay` function in the `Game` component. What should `handlePlay` do when called? Previously, the `Board` component called `setSquares` with an updated array; now it passes the updated squares array to `onPlay`. The `handlePlay` function needs to update the `Game` component's state to trigger a re-render. Instead of using `setSquares`, you'll update the `history` state variable by appending the updated squares array as a new `history` entry. You also need to toggle `xIsNext`, just as the `Board` component used to do. ```js {2-3} function handlePlay(nextSquares) { setHistory(history.concat([nextSquares])) setXIsNext(!xIsNext) } ``` At this point, you've moved the state to live in the `Game` component, and the UI should be fully working, just as it was before the refactor. Here is what the code should look like at this point: ```jsx import { create } from 'zustand' import { combine } from 'zustand/middleware' const useGameStore = create( combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }), ) function Square({ value, onSquareClick }) { return ( ) } function Board({ xIsNext, squares, onPlay }) { const winner = calculateWinner(squares) const turns = calculateTurns(squares) const player = xIsNext ? 'X' : 'O' const status = calculateStatus(winner, turns, player) function handleClick(i) { if (squares[i] || winner) return const nextSquares = squares.slice() nextSquares[i] = player onPlay(nextSquares) } return ( <>
{status}
{squares.map((square, squareIndex) => ( handleClick(squareIndex)} /> ))}
) } export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] function handlePlay(nextSquares) { setHistory(history.concat([nextSquares])) setXIsNext(!xIsNext) } return (
    {/*TODO*/}
) } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a] } } return null } function calculateTurns(squares) { return squares.filter((square) => !square).length } function calculateStatus(winner, turns, player) { if (!winner && !turns) return 'Draw' if (winner) return `Winner ${winner}` return `Next player: ${player}` } ``` ### Showing the past moves Since you are recording the tic-tac-toe game's history, you can now display a list of past moves to the player. You already have an array of `history` moves in store, so now you need to transform it to an array of React elements. In JavaScript, to transform one array into another, you can use the Array `.map()` method: You'll use `map` to transform your `history` of moves into React elements representing buttons on the screen, and display a list of buttons to **jump** to past moves. Let's `map` over the `history` in the `Game` component: ```jsx {29-44} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[history.length - 1] function handlePlay(nextSquares) { setHistory(history.concat([nextSquares])) setXIsNext(!xIsNext) } function jumpTo(nextMove) { // TODO } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } ``` Before you can implement the `jumpTo` function, you need the `Game` component to keep track of which step the user is currently viewing. To do this, define a new state variable called `currentMove`, which will start at `0`: ```js {3,14-21} const useGameStore = create( combine( { history: [Array(9).fill(null)], currentMove: 0, xIsNext: true }, (set) => { return { setHistory: (nextHistory) => { set((state) => ({ history: typeof nextHistory === 'function' ? nextHistory(state.history) : nextHistory, })) }, setCurrentMove: (nextCurrentMove) => { set((state) => ({ currentMove: typeof nextCurrentMove === 'function' ? nextCurrentMove(state.currentMove) : nextCurrentMove, })) }, setXIsNext: (nextXIsNext) => { set((state) => ({ xIsNext: typeof nextXIsNext === 'function' ? nextXIsNext(state.xIsNext) : nextXIsNext, })) }, } }, ), ) ``` Next, update the `jumpTo` function inside `Game` component to update that `currentMove`. You’ll also set `xIsNext` to `true` if the number that you’re changing `currentMove` to is even. ```js {2-3} function jumpTo(nextMove) { setCurrentMove(nextMove) setXIsNext(currentMove % 2 === 0) } ``` You will now make two changes to the `handlePlay` function in the `Game` component, which is called when you click on a square. - If you "go back in time" and then make a new move from that point, you only want to keep the history up to that point. Instead of adding `nextSquares` after all items in the history (using the Array `.concat()` method), you'll add it after all items in `history.slice(0, currentMove + 1)` to keep only that portion of the old history. - Each time a move is made, you need to update `currentMove` to point to the latest history entry. ```js {2-4} function handlePlay(nextSquares) { const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) setXIsNext(!xIsNext) } ``` Finally, you will modify the `Game` component to render the currently selected move, instead of always rendering the final move: ```jsx {2-8} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const currentMove = useGameStore((state) => state.currentMove) const setCurrentMove = useGameStore((state) => state.setCurrentMove) const xIsNext = useGameStore((state) => state.xIsNext) const setXIsNext = useGameStore((state) => state.setXIsNext) const currentSquares = history[currentMove] function handlePlay(nextSquares) { const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) setXIsNext(!xIsNext) } function jumpTo(nextMove) { setCurrentMove(nextMove) setXIsNext(nextMove % 2 === 0) } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } ``` ### Final cleanup If you look closely at the code, you'll see that `xIsNext` is `true` when `currentMove` is even and `false` when `currentMove` is odd. This means that if you know the value of `currentMove`, you can always determine what `xIsNext` should be. There's no need to store `xIsNext` separately in the state. It’s better to avoid redundant state because it can reduce bugs and make your code easier to understand. Instead, you can calculate `xIsNext` based on `currentMove`: ```jsx {2-5,13,17} export default function Game() { const history = useGameStore((state) => state.history) const setHistory = useGameStore((state) => state.setHistory) const currentMove = useGameStore((state) => state.currentMove) const setCurrentMove = useGameStore((state) => state.setCurrentMove) const xIsNext = currentMove % 2 === 0 const currentSquares = history[currentMove] function handlePlay(nextSquares) { const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) setHistory(nextHistory) setCurrentMove(nextHistory.length - 1) } function jumpTo(nextMove) { setCurrentMove(nextMove) } return (
    {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return (
  1. ) })}
) } ``` You no longer need the `xIsNext` state declaration or the calls to `setXIsNext`. Now, there’s no chance for `xIsNext` to get out of sync with `currentMove`, even if you make a mistake while coding the components. ### Wrapping up Congratulations! You’ve created a tic-tac-toe game that: - Lets you play tic-tac-toe, - Indicates when a player has won the game or when is drawn, - Stores a game’s history as a game progresses, - Allows players to review a game’s history and see previous versions of a game’s board. Nice work! We hope you now feel like you have a decent grasp of how React and Zustand works. ]]> void updateLastName: (lastName: State['lastName']) => void } // Create your store, which includes both state and (optionally) actions const usePersonStore = create((set) => ({ firstName: '', lastName: '', updateFirstName: (firstName) => set(() => ({ firstName: firstName })), updateLastName: (lastName) => set(() => ({ lastName: lastName })), })) // In consuming app function App() { // "select" the needed state and actions, in this case, the firstName value // and the action updateFirstName const firstName = usePersonStore((state) => state.firstName) const updateFirstName = usePersonStore((state) => state.updateFirstName) return (

Hello, {firstName}!

) } ``` ## Deeply nested object If you have a deep state object like this: ```ts type State = { deep: { nested: { obj: { count: number } } } } ``` Updating nested state requires some effort to ensure the process is completed immutably. ### Normal approach Similar to React or Redux, the normal approach is to copy each level of the state object. This is done with the spread operator `...`, and by manually merging that in with the new state values. Like so: ```ts normalInc: () => set((state) => ({ deep: { ...state.deep, nested: { ...state.deep.nested, obj: { ...state.deep.nested.obj, count: state.deep.nested.obj.count + 1 } } } })), ``` This is very long! Let's explore some alternatives that will make your life easier. ### With Immer Many people use [Immer](https://github.com/immerjs/immer) to update nested values. Immer can be used anytime you need to update nested state such as in React, Redux and of course, Zustand! You can use Immer to shorten your state updates for deeply nested object. Let's take a look at an example: ```ts immerInc: () => set(produce((state: State) => { ++state.deep.nested.obj.count })), ``` What a reduction! Please take note of the [gotchas listed here](../../reference/integrations/immer-middleware.md). ### With optics-ts There is another option with [optics-ts](https://github.com/akheron/optics-ts/): ```ts opticsInc: () => set(O.modify(O.optic().path("deep.nested.obj.count"))((c) => c + 1)), ``` Unlike Immer, optics-ts doesn't use proxies or mutation syntax. ### With Ramda You can also use [Ramda](https://ramdajs.com/): ```ts ramdaInc: () => set(R.modifyPath(["deep", "nested", "obj", "count"], (c) => c + 1)), ``` Both ramda and optics-ts also work with types. ### Demo https://stackblitz.com/edit/vitejs-vite-j6bjdygu ]]>
({ count: 0, text: 'hello', inc: () => set((state) => ({ count: state.count + 1 })), setText: (text) => set({ text }), })) ``` This creates a self-contained store with data and actions together. --- An alternative approach is to define actions at module level, external to the store. ```js export const useBoundStore = create(() => ({ count: 0, text: 'hello', })) export const inc = () => useBoundStore.setState((state) => ({ count: state.count + 1 })) export const setText = (text) => useBoundStore.setState({ text }) ``` This has a few advantages: - It doesn't require a hook to call an action; - It facilitates code splitting. While this pattern doesn't offer any downsides, some may prefer colocating due to its encapsulated nature. ]]> ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) ``` Another individual store: ```js export const createBearSlice = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) ``` You can now combine both the stores into **one bounded store**: ```js import { create } from 'zustand' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' export const useBoundStore = create((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), })) ``` ### Usage in a React component ```jsx import { useBoundStore } from './stores/useBoundStore' function App() { const bears = useBoundStore((state) => state.bears) const fishes = useBoundStore((state) => state.fishes) const addBear = useBoundStore((state) => state.addBear) return (

Number of bears: {bears}

Number of fishes: {fishes}

) } export default App ``` ### Updating multiple stores You can update multiple stores, at the same time, in a single function. ```js export const createBearFishSlice = (set, get) => ({ addBearAndFish: () => { get().addBear() get().addFish() }, }) ``` Combining all the stores together is the same as before. ```js import { create } from 'zustand' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' import { createBearFishSlice } from './createBearFishSlice' export const useBoundStore = create((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createBearFishSlice(...a), })) ``` ## Adding middlewares Adding middlewares to a combined store is the same as with other normal stores. Adding [`persist` middleware](../../reference/integrations/persisting-store-data.md) to our `useBoundStore`: ```js import { create } from 'zustand' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' import { persist } from 'zustand/middleware' export const useBoundStore = create( persist( (...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), }), { name: 'bound-store' }, ), ) ``` Please keep in mind you should only apply middlewares in the combined store. Applying them inside individual slices can lead to unexpected issues. ## Usage with TypeScript A detailed guide on how to use the slice pattern in Zustand with TypeScript can be found [here](./advanced-typescript.md#slices-pattern). ]]>
({ count: 0, inc: () => set((state) => ({ count: state.count + 1 })), })) ``` The `set` function is to update state in the store. Because the state is immutable, it should have been like this: ```js set((state) => ({ ...state, count: state.count + 1 })) ``` However, as this is a common pattern, `set` actually merges state, and we can skip the `...state` part: ```js set((state) => ({ count: state.count + 1 })) ``` ## Nested objects The `set` function merges state at only one level. If you have a nested object, you need to merge them explicitly. You will use the spread operator pattern like so: ```jsx import { create } from 'zustand' const useCountStore = create((set) => ({ nested: { count: 0 }, inc: () => set((state) => ({ nested: { ...state.nested, count: state.nested.count + 1 }, })), })) ``` For complex use cases, consider using some libraries that help with immutable updates. You can refer to [Updating nested state object values](./updating-state.md#deeply-nested-object). ## Replace flag To disable the merging behavior, you can specify a `replace` boolean value for `set` like so: ```js set((state) => newState, true) ``` ]]> state.foo) ``` ### Updating a Map Always create a new Map instance: ```ts // Update single entry set((state) => ({ foo: new Map(state.foo).set(key, value), })) // Delete entry set((state) => { const next = new Map(state.foo) next.delete(key) return { foo: next } }) // Update multiple entries set((state) => { const next = new Map(state.foo) next.set('key1', 'value1') next.set('key2', 'value2') return { foo: next } }) // Clear set({ foo: new Map() }) ``` ## Set ### Reading a Set ```ts const bar = useSomeStore((state) => state.bar) ``` ### Updating a Set Always create a new Set instance: ```ts // Add item set((state) => ({ bar: new Set(state.bar).add(item), })) // Delete item set((state) => { const next = new Set(state.bar) next.delete(item) return { bar: next } }) // Toggle item set((state) => { const next = new Set(state.bar) next.has(item) ? next.delete(item) : next.add(item) return { bar: next } }) // Clear set({ bar: new Set() }) ``` ## Why New Instances? Zustand detects changes by comparing references. Mutating a Map or Set doesn't change its reference: ```ts // ❌ Wrong - same reference, no re-render set((state) => { state.foo.set(key, value) return { foo: state.foo } }) // ✅ Correct - new reference, triggers re-render set((state) => ({ foo: new Map(state.foo).set(key, value), })) ``` ## Pitfall: Type Hints for Empty Collections Provide type hints when initializing empty Maps and Sets: ```ts { ids: new Set([] as string[]), users: new Map([] as [string, User][]) } ``` Without type hints, TypeScript infers `never[]` which prevents adding items later. ## Demos Basic: https://stackblitz.com/edit/vitejs-vite-5cu5ddvx ]]> ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', littleBear: 'A little, small, wee pot', })) export const BearNames = () => { const names = useMeals((state) => Object.keys(state)) return
{names.join(', ')}
} ``` Now papa bear wants a pizza instead: ```js useMeals.setState({ papaBear: 'a large pizza', }) ``` This change causes `BearNames` rerenders even though the actual output of `names` has not changed according to shallow equal. We can fix that using `useShallow`! ```js import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' const useMeals = create(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', littleBear: 'A little, small, wee pot', })) export const BearNames = () => { const names = useMeals(useShallow((state) => Object.keys(state))) return
{names.join(', ')}
} ``` Now they can all order other meals without causing unnecessary rerenders of our `BearNames` component. ]]>
{ const searchParams = new URLSearchParams(location.hash.slice(1)) const storedValue = searchParams.get(key) ?? '' return JSON.parse(storedValue) }, setItem: (key, newValue): void => { const searchParams = new URLSearchParams(location.hash.slice(1)) searchParams.set(key, JSON.stringify(newValue)) location.hash = searchParams.toString() }, removeItem: (key): void => { const searchParams = new URLSearchParams(location.hash.slice(1)) searchParams.delete(key) location.hash = searchParams.toString() }, } export const useBoundStore = create()( persist( (set, get) => ({ fishes: 0, addAFish: () => set({ fishes: get().fishes + 1 }), }), { name: 'food-storage', // unique name storage: createJSONStorage(() => hashStorage), }, ), ) ``` ## Persist and Connect State with URL Parameters (Example: URL Query Parameters) There are times when you want to conditionally connect the state to the URL. This example depicts usage of the URL query parameters while keeping it synced with another persistence implementation, like `localstorage`. If you want the URL params to always populate, the conditional check on `getUrlSearch()` can be removed. The implementation below will update the URL in place, without refresh, as the relevant states change. ```ts import { create } from 'zustand' import { persist, StateStorage, createJSONStorage } from 'zustand/middleware' const getUrlSearch = () => { return window.location.search.slice(1) } const persistentStorage: StateStorage = { getItem: (key): string => { // Check URL first if (getUrlSearch()) { const searchParams = new URLSearchParams(getUrlSearch()) const storedValue = searchParams.get(key) return JSON.parse(storedValue as string) } else { // Otherwise, we should load from localstorage or alternative storage return JSON.parse(localStorage.getItem(key) as string) } }, setItem: (key, newValue): void => { // Check if query params exist at all, can remove check if always want to set URL if (getUrlSearch()) { const searchParams = new URLSearchParams(getUrlSearch()) searchParams.set(key, JSON.stringify(newValue)) window.history.replaceState(null, '', `?${searchParams.toString()}`) } localStorage.setItem(key, JSON.stringify(newValue)) }, removeItem: (key): void => { const searchParams = new URLSearchParams(getUrlSearch()) searchParams.delete(key) window.location.search = searchParams.toString() }, } type LocalAndUrlStore = { typesOfFish: string[] addTypeOfFish: (fishType: string) => void numberOfBears: number setNumberOfBears: (newNumber: number) => void } const storageOptions = { name: 'fishAndBearsStore', storage: createJSONStorage(() => persistentStorage), } const useLocalAndUrlStore = create()( persist( (set) => ({ typesOfFish: [], addTypeOfFish: (fishType) => set((state) => ({ typesOfFish: [...state.typesOfFish, fishType] })), numberOfBears: 0, setNumberOfBears: (numberOfBears) => set(() => ({ numberOfBears })), }), storageOptions, ), ) export default useLocalAndUrlStore ``` When generating the URL from a component, you can call buildShareableUrl: ```ts const buildURLSuffix = (params, version = 0) => { const searchParams = new URLSearchParams() const zustandStoreParams = { state: { typesOfFish: params.typesOfFish, numberOfBears: params.numberOfBears, }, version: version, // version is here because that is included with how Zustand sets the state } // The URL param key should match the name of the store, as specified as in storageOptions above searchParams.set('fishAndBearsStore', JSON.stringify(zustandStoreParams)) return searchParams.toString() } export const buildShareableUrl = (params, version) => { return `${window.location.origin}?${buildURLSuffix(params, version)}` } ``` The generated URL would look like (here without any encoding, for readability): `https://localhost/search?fishAndBearsStore={"state":{"typesOfFish":["tilapia","salmon"],"numberOfBears":15},"version":0}}` ### Demo - Hash: https://stackblitz.com/edit/vitejs-vite-9vg24prg - Query: https://stackblitz.com/edit/vitejs-vite-hyc97ynf ]]> ({ fishes: 0, increaseFishes: () => set((prev) => ({ fishes: prev.fishes + 1 })), })) const nonReactCallback = () => { unstable_batchedUpdates(() => { useFishStore.getState().increaseFishes() }) } ``` More details: https://github.com/pmndrs/zustand/issues/302 ]]> ` generic forces the store to match this shape. This means if you forget a field or use the wrong type, TypeScript will complain. Unlike plain JS, this guarantees type-safe state management. The `create` function uses the curried form, which results in a store of type `UseBoundStore>`. ```ts // store.ts import { create } from 'zustand' // Define types for state & actions interface BearState { bears: number food: string feed: (food: string) => void } // Create store using the curried form of `create` export const useBearStore = create()((set) => ({ bears: 2, food: 'honey', feed: (food) => set(() => ({ food })), })) ``` ### Using the Store in Components Inside components, you can read state and call actions. Selectors `(s) => s.bears` subscribe to only what you need. This reduces re-renders and improves performance. JS can do this too, but with TS your IDE autocompletes state fields. ```tsx import { useBearStore } from './store' function BearCounter() { // Select only 'bears' to avoid unnecessary re-renders const bears = useBearStore((s) => s.bears) return

{bears} bears around

} ``` ### Resetting the Store Resetting is useful after logout or “clear session”. We use `typeof initialState` to avoid repeating property types. TypeScript updates automatically if `initialState` changes. This is safer and cleaner compared to JS. ```tsx import { create } from 'zustand' const initialState = { bears: 0, food: 'honey' } // Reuse state type dynamically type BearState = typeof initialState & { increase: (by: number) => void reset: () => void } const useBearStore = create()((set) => ({ ...initialState, increase: (by) => set((s) => ({ bears: s.bears + by })), reset: () => set(initialState), })) function ResetZoo() { const { bears, increase, reset } = useBearStore() return (
{bears}
) } ``` ### Extracting Types Zustand provides a built-in helper called `ExtractState`. This is useful for tests, utility functions, or component props. It returns the full type of your store’s state and actions without having to manually redefine them. Extracting the Store type: ```ts // store.ts import { create, type ExtractState } from 'zustand' export const useBearStore = create((set) => ({ bears: 3, food: 'honey', increase: (by: number) => set((s) => ({ bears: s.bears + by })), })) // Extract the type of the whole store state export type BearState = ExtractState ``` Using extracted type in tests: ```ts // test.cy.ts import { BearState } from './store.ts' test('should reset store', () => { const snapshot: BearState = useBearStore.getState() expect(snapshot.bears).toBeGreaterThanOrEqual(0) }) ``` and in utility function: ```ts // util.ts import { BearState } from './store.ts' function logBearState(state: BearState) { console.log(`We have ${state.bears} bears eating ${state.food}`) } logBearState(useBearStore.getState()) ``` ### Selectors #### Multiple Selectors Sometimes you need more than one property. Returning an object from the selector lets you access multiple fields at once. However, directly destructuring properties from that object can cause unnecessary re-renders. To avoid this, it’s recommended to wrap the selector with `useShallow`, which prevents re-renders when the selected values remain shallowly equal. This is more efficient than subscribing to the whole store. TypeScript ensures you can’t accidentally misspell `bears` or `food`. See the [API documentation](https://zustand.docs.pmnd.rs/hooks/use-shallow) for more details on `useShallow`. ```tsx import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' // Bear store with explicit types interface BearState { bears: number food: number } const useBearStore = create()(() => ({ bears: 2, food: 10, })) // In components, you can use both stores safely function MultipleSelectors() { const { bears, food } = useBearStore( useShallow((state) => ({ bears: state.bears, food: state.food })), ) return (
We have {food} units of food for {bears} bears
) } ``` #### Derived State with Selectors Not all values need to be stored directly - some can be computed from existing state. You can derive values using selectors. This avoids duplication and keeps the store minimal. TypeScript ensures `bears` is a number, so math is safe. ```tsx import { create } from 'zustand' interface BearState { bears: number foodPerBear: number } const useBearStore = create()(() => ({ bears: 3, foodPerBear: 2, })) function TotalFood() { // Derived value: required amount food for all bears const totalFood = useBearStore((s) => s.bears * s.foodPerBear) // don't need to have extra property `{ totalFood: 6 }` in your Store return
We need ${totalFood} jars of honey
} ``` ### Middlewares #### `combine` middleware This middleware separates initial state and actions, making the code cleaner. TS automatically infers types from the state and actions, no interface needed. This is different from JS, where type safety is missing. It’s a very popular style in TypeScript projects. See the [API documentation](https://zustand.docs.pmnd.rs/middlewares/combine) for more details. ```ts import { create } from 'zustand' import { combine } from 'zustand/middleware' interface BearState { bears: number increase: () => void } // State + actions are separated export const useBearStore = create()( combine({ bears: 0 }, (set) => ({ increase: () => set((s) => ({ bears: s.bears + 1 })), })), ) ``` #### `devtools` middleware This middleware connects Zustand to Redux DevTools. You can inspect changes, time-travel, and debug state. It’s extremely useful in development. TS ensures your actions and state remain type-checked even here. See the [API documentation](https://zustand.docs.pmnd.rs/middlewares/devtools) for more details. ```ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' interface BearState { bears: number increase: () => void } export const useBearStore = create()( devtools((set) => ({ bears: 0, increase: () => set((s) => ({ bears: s.bears + 1 })), })), ) ``` #### `persist` middleware This middleware keeps your store in `localStorage` (or another storage). This means your bears survive a page refresh. Great for apps where persistence matters. In TS, the state type stays consistent, so no runtime surprises. See the [API documentation](https://zustand.docs.pmnd.rs/middlewares/persist) for more details. ```ts import { create } from 'zustand' import { persist } from 'zustand/middleware' interface BearState { bears: number increase: () => void } export const useBearStore = create()( persist( (set) => ({ bears: 0, increase: () => set((s) => ({ bears: s.bears + 1 })), }), { name: 'bear-storage' }, // localStorage key ), ) ``` ### Async Actions Actions can be async to fetch remote data. Here we fetch bears count and update state. TS enforces correct API response type (`BearData`). In JS you might misspell `count` - TS prevents that. ```ts import { create } from 'zustand' interface BearData { count: number } interface BearState { bears: number fetchBears: () => Promise } export const useBearStore = create()((set) => ({ bears: 0, fetchBears: async () => { const res = await fetch('/api/bears') const data: BearData = await res.json() set({ bears: data.count }) }, })) ``` ### `createWithEqualityFn` Variant of `create` with equality built-in. Useful if you always want custom equality checks. Not common, but shows Zustand’s flexibility. TS still keeps full type inference. See the [API documentation](https://zustand.docs.pmnd.rs/apis/create-with-equality-fn) for more details. ```ts import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' const useBearStore = createWithEqualityFn(() => ({ bears: 0, })) const bears = useBearStore((s) => s.bears, Object.is) // or const bears = useBearStore((s) => ({ bears: s.bears }), shallow) ``` ### Multiple Stores You can create more than one store for different domains. For example, `BearStore` manages bears and `FishStore` manages fish. This keeps state isolated and easier to maintain in larger apps. With TypeScript, each store has its own strict type - you can’t accidentally mix bears and fish. ```tsx import { create } from 'zustand' // Bear store with explicit types interface BearState { bears: number addBear: () => void } const useBearStore = create()((set) => ({ bears: 2, addBear: () => set((s) => ({ bears: s.bears + 1 })), })) // Fish store with explicit types interface FishState { fish: number addFish: () => void } const useFishStore = create()((set) => ({ fish: 5, addFish: () => set((s) => ({ fish: s.fish + 1 })), })) // In components, you can use both stores safely function Zoo() { const { bears, addBear } = useBearStore() const { fish, addFish } = useFishStore() return (
{bears} bears and {fish} fish
) } ``` ### Conclusion Zustand together with TypeScript provides a balance: you keep the simplicity of small, minimalistic stores, while gaining the safety of strong typing. You don’t need boilerplate or complex patterns - state and actions live side by side, fully typed, and ready to use. Start with a basic store to learn the pattern, then expand gradually: use `combine` for cleaner inference, `persist` for storage, and `devtools` for debugging. ]]>
()(...)` (notice the extra parentheses `()` too along with the type parameter) where `T` is the type of the state to annotate it. For example: ```ts import { create } from 'zustand' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) ```
Why can't we simply infer the type from the initial state?
**TLDR**: Because state generic `T` is invariant. Consider this minimal version `create`: ```ts declare const create: (f: (get: () => T) => T) => T const x = create((get) => ({ foo: 0, bar: () => get(), })) // `x` is inferred as `unknown` instead of // interface X { // foo: number, // bar: () => X // } ``` Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it "gives" `T` via return (making it covariant), but it also "takes" `T` via `get` (making it contravariant). "So where does `T` come from?" TypeScript wonders. It's like that chicken or egg problem. At the end TypeScript, gives up and infers `T` as `unknown`. So, as long as the generic to be inferred is invariant (i.e. both covariant and contravariant), TypeScript will be unable to infer it. Another simple example would be this: ```ts const createFoo = {} as (f: (t: T) => T) => T const x = createFoo((_) => 'hello') ``` Here again, `x` is `unknown` instead of `string`.
More about the inference (just for the people curious and interested in TypeScript) In some sense this inference failure is not a problem because a value of type `(f: (t: T) => T) => T` cannot be written. That is to say you can't write the real runtime implementation of `createFoo`. Let's try it: ```js const createFoo = (f) => f(/* ? */) ``` `createFoo` needs to return the return value of `f`. And to do that we first have to call `f`. And to call it we have to pass a value of type `T`. And to pass a value of type `T` we first have to produce it. But how can we produce a value of type `T` when we don't even know what `T` is? The only way to produce a value of type `T` is to call `f`, but then to call `f` itself we need a value of type `T`. So you see it's impossible to actually write `createFoo`. So what we're saying is, the inference failure in case of `createFoo` is not really a problem because it's impossible to implement `createFoo`. But what about the inference failure in case of `create`? That also is not really a problem because it's impossible to implement `create` too. Wait a minute, if it's impossible to implement `create` then how does Zustand implement it? The answer is, it doesn't. Zustand lies that it implemented `create`'s type, it implemented only the most part of it. Here's a simple proof by showing unsoundness. Consider the following code: ```ts import { create } from 'zustand' const useBoundStore = create<{ foo: number }>()((_, get) => ({ foo: get().foo, })) ``` This code compiles. But if we run it, we'll get an exception: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". This is because `get` would return `undefined` before the initial state is created (hence you shouldn't call `get` when creating the initial state). The types promise that `get` will never return `undefined` but it does initially, which means Zustand failed to implement it. And of course Zustand failed because it's impossible to implement `create` the way types promise (in the same way it's impossible to implement `createFoo`). In other words we don't have a type to express the actual `create` we have implemented. We can't type `get` as `() => T | undefined` because it would cause inconvenience and it still won't be correct as `get` is indeed `() => T` eventually, just if called synchronously it would be `() => undefined`. What we need is some kind of TypeScript feature that allows us to type `get` as `(() => T) & WhenSync<() => undefined>`, which of course is extremely far-fetched. So we have two problems: lack of inference and unsoundness. Lack of inference can be solved if TypeScript can improve its inference for invariants. And unsoundness can be solved if TypeScript introduces something like `WhenSync`. To work around lack of inference we manually annotate the state type. And we can't work around unsoundness, but it's not a big deal because it's not much, calling `get` synchronously anyway doesn't make sense.
Why the currying `()(...)`?
**TLDR**: It is a workaround for [microsoft/TypeScript#10571](https://github.com/microsoft/TypeScript/issues/10571). Imagine you have a scenario like this: ```ts declare const withError: ( p: Promise, ) => Promise<[error: undefined, value: T] | [error: E, value: undefined]> declare const doSomething: () => Promise const main = async () => { let [error, value] = await withError(doSomething()) } ``` Here, `T` is inferred to be a `string` and `E` is inferred to be `unknown`. You might want to annotate `E` as `Foo`, because you are certain of the shape of error `doSomething()` would throw. However, you can't do that. You can either pass all generics or none. Along with annotating `E` as `Foo`, you will also have to annotate `T` as `string` even though it gets inferred anyway. The solution is to make a curried version of `withError` that does nothing at runtime. Its purpose is to just allow you annotate `E`. ```ts declare const withError: { (): ( p: Promise, ) => Promise<[error: undefined, value: T] | [error: E, value: undefined]> ( p: Promise, ): Promise<[error: undefined, value: T] | [error: E, value: undefined]> } declare const doSomething: () => Promise interface Foo { bar: string } const main = async () => { let [error, value] = await withError()(doSomething()) } ``` This way, `T` gets inferred and you get to annotate `E`. Zustand has the same use case when we want to annotate the state (the first type parameter) but allow other parameters to get inferred.
Alternatively, you can also use `combine`, which infers the state so that you do not need to type it. ```ts import { create } from 'zustand' import { combine } from 'zustand/middleware' const useBearStore = create( combine({ bears: 0 }, (set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })), })), ) ```
Be a little careful
We achieve the inference by lying a little in the types of `set`, `get`, and `store` that you receive as parameters. The lie is that they're typed as if the state is the first parameter, when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`. It isn't really a lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be unsound as it will delete the `increase` function. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]`. The return type of `get` can make you fall for these mistakes. `combine` trades off a little type-safety for the convenience of not having to write a type for state. Hence, you should use `combine` accordingly. It is fine in most cases and you can use it conveniently.
Note that we don't use the curried version when using `combine` because `combine` "creates" the state. When using a middleware that creates the state, it isn't necessary to use the curried version because the state now can be inferred. Another middleware that creates state is `redux`. So when using `combine`, `redux`, or any other custom middleware that creates the state, we don't recommend using the curried version. If you want to infer state type also outside of state declaration, you can use the `ExtractState` type helper: ```ts import { create, ExtractState } from 'zustand' import { combine } from 'zustand/middleware' type BearState = ExtractState const useBearStore = create( combine({ bears: 0 }, (set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })), })), ) ``` ## Using middlewares You do not have to do anything special to use middlewares in TypeScript. ```ts import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()( devtools( persist( (set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), { name: 'bearStore' }, ), ), ) ``` Just make sure you are using them immediately inside `create` so as to make the contextual inference work. Doing something even remotely fancy like the following `myMiddlewares` would require more advanced types. ```ts import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' const myMiddlewares = (f) => devtools(persist(f, { name: 'bearStore' })) interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()( myMiddlewares((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })), ) ``` Also, we recommend using `devtools` middleware as last as possible. For example, when you use it with `immer` as a middleware, it should be `devtools(immer(...))` and not `immer(devtools(...))`. This is because`devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) also mutate `setState` before `devtools`. Hence using `devtools` at the end makes sure that no middlewares mutate `setState` before it. ## Authoring middlewares and advanced usage Imagine you had to write this hypothetical middleware. ```ts import { create } from 'zustand' const foo = (f, bar) => (set, get, store) => { store.foo = bar return f(set, get, store) } const useBearStore = create(foo(() => ({ bears: 0 }), 'hello')) console.log(useBearStore.foo.toUpperCase()) ``` Zustand middlewares can mutate the store. But how could we possibly encode the mutation on the type-level? That is to say how could we type `foo` so that this code compiles? For a usual statically typed language, this is impossible. But thanks to TypeScript, Zustand has something called a "higher-kinded mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710). If you are eager to know what the answer is to this particular problem then you can [see it here](#middleware-that-changes-the-store-type). ### Handling Dynamic `replace` Flag If the value of the `replace` flag is not known at compile time and is determined dynamically, you might face issues. To handle this, you can use a workaround by annotating the `replace` parameter with the parameters of the `setState` function: ```ts const replaceFlag = Math.random() > 0.5 const args = [{ bears: 5 }, replaceFlag] as Parameters< typeof useBearStore.setState > store.setState(...args) ``` #### Example with `as Parameters` Workaround ```ts import { create } from 'zustand' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) const replaceFlag = Math.random() > 0.5 const args = [{ bears: 5 }, replaceFlag] as Parameters< typeof useBearStore.setState > useBearStore.setState(...args) // Using the workaround ``` By following this approach, you can ensure that your code handles dynamic `replace` flags without encountering type issues. ## Common recipes ### Middleware that doesn't change the store type ```ts import { create, StateCreator, StoreMutatorIdentifier } from 'zustand' type Logger = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( f: StateCreator, name?: string, ) => StateCreator type LoggerImpl = ( f: StateCreator, name?: string, ) => StateCreator const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => { const loggedSet: typeof set = (...a) => { set(...(a as Parameters)) console.log(...(name ? [`${name}:`] : []), get()) } const setState = store.setState store.setState = (...a) => { setState(...(a as Parameters)) console.log(...(name ? [`${name}:`] : []), store.getState()) } return f(loggedSet, get, store) } export const logger = loggerImpl as unknown as Logger // --- const useBearStore = create()( logger( (set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), 'bear-store', ), ) ``` ### Middleware that changes the store type ```ts import { create, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi, } from 'zustand' type Foo = < T, A, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( f: StateCreator, bar: A, ) => StateCreator declare module 'zustand' { interface StoreMutators { foo: Write, { foo: A }> } } type FooImpl = ( f: StateCreator, bar: A, ) => StateCreator const fooImpl: FooImpl = (f, bar) => (set, get, _store) => { type T = ReturnType type A = typeof bar const store = _store as Mutate, [['foo', A]] > store.foo = bar return f(set, get, _store) } export const foo = fooImpl as unknown as Foo type Write = Omit & U type Cast = T extends U ? T : U // --- const useBearStore = create(foo(() => ({ bears: 0 }), 'hello')) console.log(useBearStore.foo.toUpperCase()) ``` ### `create` without curried workaround The recommended way to use `create` is using the curried workaround like so: `create()(...)`. This is because it enables you to infer the store type. But if for some reason you do not want to use the workaround, you can pass the type parameters like the following. Note that in some cases, this acts as an assertion instead of annotation, so we don't recommend it. ```ts import { create } from "zustand" interface BearState { bears: number increase: (by: number) => void } const useBearStore = create< BearState, [ ['zustand/persist', BearState], ['zustand/devtools', never] ] >(devtools(persist((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), { name: 'bearStore' })) ``` ### Slices pattern ```ts import { create, StateCreator } from 'zustand' interface BearSlice { bears: number addBear: () => void eatFish: () => void } interface FishSlice { fishes: number addFish: () => void } interface SharedSlice { addBoth: () => void getBoth: () => number } const createBearSlice: StateCreator< BearSlice & FishSlice, [], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) const createFishSlice: StateCreator< BearSlice & FishSlice, [], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) const createSharedSlice: StateCreator< BearSlice & FishSlice, [], [], SharedSlice > = (set, get) => ({ addBoth: () => { // you can reuse previous methods get().addBear() get().addFish() // or do them from scratch // set((state) => ({ bears: state.bears + 1, fishes: state.fishes + 1 }) }, getBoth: () => get().bears + get().fishes, }) const useBoundStore = create()((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createSharedSlice(...a), })) ``` A detailed explanation on the slices pattern can be found [here](./slices-pattern.md). If you have some middlewares then replace `StateCreator` with `StateCreator`. For example, if you are using `devtools` then it will be `StateCreator`. See the ["Middlewares and their mutators reference"](#middlewares-and-their-mutators-reference) section for a list of all mutators. ### Bounded `useStore` hook for vanilla stores ```ts import { useStore } from 'zustand' import { createStore } from 'zustand/vanilla' interface BearState { bears: number increase: (by: number) => void } const bearStore = createStore()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) function useBearStore(): BearState function useBearStore(selector: (state: BearState) => T): T function useBearStore(selector?: (state: BearState) => T) { return useStore(bearStore, selector!) } ``` You can also make an abstract `createBoundedUseStore` function if you need to create bounded `useStore` hooks often and want to DRY things up... ```ts import { useStore, StoreApi } from 'zustand' import { createStore } from 'zustand/vanilla' interface BearState { bears: number increase: (by: number) => void } const bearStore = createStore()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) const createBoundedUseStore = ((store) => (selector) => useStore(store, selector)) as >( store: S, ) => { (): ExtractState (selector: (state: ExtractState) => T): T } type ExtractState = S extends { getState: () => infer X } ? X : never const useBearStore = createBoundedUseStore(bearStore) ``` ## Middlewares and their mutators reference - `devtools` — `["zustand/devtools", never]` - `persist` — `["zustand/persist", YourPersistedState]`
`YourPersistedState` is the type of state you are going to persist, ie the return type of `options.partialize`, if you're not passing `partialize` options the `YourPersistedState` becomes `Partial`. Also [sometimes](https://github.com/pmndrs/zustand/issues/980#issuecomment-1162289836) passing actual `PersistedState` won't work. In those cases, try passing `unknown`. - `immer` — `["zustand/immer", never]` - `subscribeWithSelector` — `["zustand/subscribeWithSelector", never]` - `redux` — `["zustand/redux", YourAction]` - `combine` — no mutator as `combine` does not mutate the store ]]>
state.bears) ``` However, writing these could be tedious. If that is the case for you, you can auto-generate your selectors. ## Create the following function: `createSelectors` ```typescript import { StoreApi, UseBoundStore } from 'zustand' type WithSelectors = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never const createSelectors = >>( _store: S, ) => { const store = _store as WithSelectors store.use = {} for (const k of Object.keys(store.getState())) { ;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]) } return store } ``` If you have a store like this: ```typescript interface BearState { bears: number increase: (by: number) => void increment: () => void } const useBearStoreBase = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), increment: () => set((state) => ({ bears: state.bears + 1 })), })) ``` Apply that function to your store: ```typescript const useBearStore = createSelectors(useBearStoreBase) ``` Now the selectors are auto generated and you can access them directly: ```typescript // get the property const bears = useBearStore.use.bears() // get the action const increment = useBearStore.use.increment() ``` ## Vanilla Store If you are using a vanilla store, use the following `createSelectors` function: ```typescript import { StoreApi, useStore } from 'zustand' type WithSelectors = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never const createSelectors = >(_store: S) => { const store = _store as WithSelectors store.use = {} for (const k of Object.keys(store.getState())) { ;(store.use as any)[k] = () => useStore(_store, (s) => s[k as keyof typeof s]) } return store } ``` The usage is the same as a React store. If you have a store like this: ```typescript import { createStore } from 'zustand' interface BearState { bears: number increase: (by: number) => void increment: () => void } const store = createStore()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), increment: () => set((state) => ({ bears: state.bears + 1 })), })) ``` Apply that function to your store: ```typescript const useBearStore = createSelectors(store) ``` Now the selectors are auto generated and you can access them directly: ```typescript // get the property const bears = useBearStore.use.bears() // get the action const increment = useBearStore.use.increment() ``` ## Live Demo For a working example of this, see the [Code Sandbox](https://codesandbox.io/s/zustand-auto-generate-selectors-forked-rl8v5e?file=/src/selectors.ts). ## Third-party Libraries - [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) - [react-hooks-global-state](https://github.com/dai-shi/react-hooks-global-state) - [zustood](https://github.com/udecode/zustood) - [@davstack/store](https://github.com/DawidWraga/davstack) ]]> [!NOTE] > We will be updating this guide soon based on our discussion in https://github.com/pmndrs/zustand/discussions/2740. [Next.js](https://nextjs.org) is a popular server-side rendering framework for React that presents some unique challenges for using Zustand properly. Keep in mind that Zustand store is a global variable (AKA module state) making it optional to use a `Context`. These challenges include: - **Per-request store:** A Next.js server can handle multiple requests simultaneously. This means that the store should be created per request and should not be shared across requests. - **SSR friendly:** Next.js applications are rendered twice, first on the server and again on the client. Having different outputs on both the client and the server will result in "hydration errors." The store will have to be initialized on the server and then re-initialized on the client with the same data in order to avoid that. Please read more about that in our [SSR and Hydration](./ssr-and-hydration.md) guide. - **SPA routing friendly:** Next.js supports a hybrid model for client side routing, which means that in order to reset a store, we need to initialize it at the component level using a `Context`. - **Server caching friendly:** Recent versions of Next.js (specifically applications using the App Router architecture) support aggressive server caching. Due to our store being a **module state**, it is completely compatible with this caching. We have these general recommendations for the appropriate use of Zustand: - **No global stores** - Because the store should not be shared across requests, it should not be defined as a global variable. Instead, the store should be created per request. - **React Server Components should not read from or write to the store** - RSCs cannot use hooks or context. They aren't meant to be stateful. Having an RSC read from or write values to a global store violates the architecture of Next.js. ### Creating a store per request Let's write our store factory function that will create a new store for each request. ```json // tsconfig.json { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ``` > **Note:** do not forget to remove all comments from your `tsconfig.json` file. ### Initializing the store ```ts // src/stores/counter-store.ts import { createStore } from 'zustand/vanilla' export type CounterState = { count: number } export type CounterActions = { decrementCount: () => void incrementCount: () => void } export type CounterStore = CounterState & CounterActions export const defaultInitState: CounterState = { count: 0, } export const createCounterStore = ( initState: CounterState = defaultInitState, ) => { return createStore()((set) => ({ ...initState, decrementCount: () => set((state) => ({ count: state.count - 1 })), incrementCount: () => set((state) => ({ count: state.count + 1 })), })) } ``` ### Providing the store Let's use the `createCounterStore` in our component and share it using a context provider. ```tsx // src/providers/counter-store-provider.tsx 'use client' import { type ReactNode, createContext, useState, useContext } from 'react' import { useStore } from 'zustand' import { type CounterStore, createCounterStore } from '@/stores/counter-store' export type CounterStoreApi = ReturnType export const CounterStoreContext = createContext( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const [store] = useState(() => createCounterStore()) return ( {children} ) } export const useCounterStore = ( selector: (store: CounterStore) => T, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (!counterStoreContext) { throw new Error(`useCounterStore must be used within CounterStoreProvider`) } return useStore(counterStoreContext, selector) } ``` > **Note:** In this example, we ensure that this component is re-render-safe by checking the > value of the reference, so that the store is only created once. This component will only be > rendered once per request on the server, but might be re-rendered multiple times on the client if > there are stateful client components located above this component in the tree, or if this component > also contains other mutable state that causes a re-render. ### Using the store with different architectures There are two architectures for a Next.js application: the [Pages Router](https://nextjs.org/docs/pages/building-your-application/routing) and the [App Router](https://nextjs.org/docs/app/building-your-application/routing). The usage of Zustand on both architectures should be the same with slight differences related to each architecture. #### Pages Router ```tsx // src/components/pages/home-page.tsx import { useCounterStore } from '@/providers/counter-store-provider' export const HomePage = () => { const { count, incrementCount, decrementCount } = useCounterStore( (state) => state, ) return (
Count: {count}
) } ``` ```tsx // src/_app.tsx import type { AppProps } from 'next/app' import { CounterStoreProvider } from '@/providers/counter-store-provider' export default function App({ Component, pageProps }: AppProps) { return ( ) } ``` ```tsx // src/pages/index.tsx import { HomePage } from '@/components/pages/home-page' export default function Home() { return } ``` > **Note:** creating a store per route would require creating and sharing the store > at page (route) component level. Try not to use this if you do not need to create > a store per route. ```tsx // src/pages/index.tsx import { CounterStoreProvider } from '@/providers/counter-store-provider' import { HomePage } from '@/components/pages/home-page' export default function Home() { return ( ) } ``` #### App Router ```tsx // src/components/pages/home-page.tsx 'use client' import { useCounterStore } from '@/providers/counter-store-provider' export const HomePage = () => { const { count, incrementCount, decrementCount } = useCounterStore( (state) => state, ) return (
Count: {count}
) } ``` ```tsx // src/app/layout.tsx import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' import { CounterStoreProvider } from '@/providers/counter-store-provider' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Create Next App', description: 'Generated by create next app', } export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( {children} ) } ``` ```tsx // src/app/page.tsx import { HomePage } from '@/components/pages/home-page' export default function Home() { return } ``` > **Note:** creating a store per route would require creating and sharing the store > at page (route) component level. Try not to use this if you do not need to create > a store per route. ```tsx // src/app/page.tsx import { CounterStoreProvider } from '@/providers/counter-store-provider' import { HomePage } from '@/components/pages/home-page' export default function Home() { return ( ) } ``` ]]>
**Note:** do not forget to remove all comments from your `tsconfig.json` file. ```tsx // app.tsx export const App = () => { return ( Static Server-side-rendered App
Hello World!
) } ``` ```tsx // server.tsx import express from 'express' import React from 'react' import ReactDOMServer from 'react-dom/server' import { App } from './app.tsx' const port = Number.parseInt(process.env.PORT || '3000', 10) const app = express() app.get('/', (_, res) => { const { pipe } = ReactDOMServer.renderToPipeableStream(, { onShellReady() { res.setHeader('content-type', 'text/html') pipe(res) }, }) }) app.listen(port, () => { console.log(`Server is listening at ${port}`) }) ``` ```sh tsc --build ``` ```sh node server.js ``` ## Hydration Hydration turns the initial HTML snapshot from the server into a fully interactive app that runs in the browser. The right way to "hydrate" a component is by using `hydrateRoot`. ### React Let's say we want to render a stateful app using React. In order to do that we need to use `express`, `react`, `react-dom/server` and `react-dom/client`. Let's dive into that: - `express` helps us build a web app that we can run using Node, - `react` helps us build the UI components that we use in our app, - `react-dom/server` helps us render our components on a server, - `react-dom/client` helps us hydrate our components on a client. > **Note:** Do not forget that even if we can render our components on a server, it is > important to "hydrate" them on a client to make them interactive. ```json // tsconfig.json { "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "target": "esnext" }, "include": ["**/*"] } ``` > **Note:** do not forget to remove all comments in your `tsconfig.json` file. ```tsx // app.tsx export const App = () => { return ( Static Server-side-rendered App
Hello World!
) } ``` ```tsx // main.tsx import ReactDOMClient from 'react-dom/client' import { App } from './app.tsx' ReactDOMClient.hydrateRoot(document, ) ``` ```tsx // server.tsx import express from 'express' import React from 'react' import ReactDOMServer from 'react-dom/server' import { App } from './app.tsx' const port = Number.parseInt(process.env.PORT || '3000', 10) const app = express() app.use('/', (_, res) => { const { pipe } = ReactDOMServer.renderToPipeableStream(, { bootstrapScripts: ['/main.js'], onShellReady() { res.setHeader('content-type', 'text/html') pipe(res) }, }) }) app.listen(port, () => { console.log(`Server is listening at ${port}`) }) ``` ```sh tsc --build ``` ```sh node server.js ``` > **Warning:** The React tree you pass to `hydrateRoot` needs to produce the same output as it did on the server. > The most common causes leading to hydration errors include: > > - Extra whitespace (like newlines) around the React-generated HTML inside the root node. > - Using checks like typeof window !== 'undefined' in your rendering logic. > - Using browser-only APIs like `window.matchMedia` in your rendering logic. > - Rendering different data on the server and the client. > > React recovers from some hydration errors, but you must fix them like other bugs. In the best case, they’ll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements. You can read more about the caveats and pitfalls here: [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot) ]]>
void } type BearStore = ReturnType const createBearStore = (initProps?: Partial) => { const DEFAULT_PROPS: BearProps = { bears: 0, } return createStore()((set) => ({ ...DEFAULT_PROPS, ...initProps, addBear: () => set((state) => ({ bears: ++state.bears })), })) } ``` ## Creating a context with `React.createContext` ```ts import { createContext } from 'react' export const BearContext = createContext(null) ``` ## Basic component usage ```tsx // Provider implementation import { useState } from 'react' function App() { const [store] = useState(() => createBearStore()) return ( ) } ``` ```tsx // Consumer component import { useContext } from 'react' import { useStore } from 'zustand' function BasicConsumer() { const store = useContext(BearContext) if (!store) throw new Error('Missing BearContext.Provider in the tree') const bears = useStore(store, (s) => s.bears) const addBear = useStore(store, (s) => s.addBear) return ( <>
{bears} Bears.
) } ``` ## Common patterns ### Wrapping the context provider ```tsx // Provider wrapper import { useState } from 'react' type BearProviderProps = React.PropsWithChildren function BearProvider({ children, ...props }: BearProviderProps) { const [store] = useState(() => createBearStore(props)) return {children} } ``` ### Extracting context logic into a custom hook ```tsx // Mimic the hook returned by `create` import { useContext } from 'react' import { useStore } from 'zustand' function useBearContext(selector: (state: BearState) => T): T { const store = useContext(BearContext) if (!store) throw new Error('Missing BearContext.Provider in the tree') return useStore(store, selector) } ``` ```tsx // Consumer usage of the custom hook function CommonConsumer() { const bears = useBearContext((s) => s.bears) const addBear = useBearContext((s) => s.addBear) return ( <>
{bears} Bears.
) } ``` ### Optionally use memoized selector for stable outputs ```tsx import { useShallow } from 'zustand/react/shallow' const meals = ['Salmon', 'Berries', 'Nuts'] function CommonConsumer() { const bearMealsOrder = useBearContext( useShallow((s) => Array.from({ length: s.bears }).map((_, index) => meals.at(index % meals.length), ), ), ) return ( <> Order:
    {bearMealsOrder.map((meal) => (
  • {meal}
  • ))}
) } ``` ### Optionally allow using a custom equality function ```tsx // Allow custom equality function by using useStoreWithEqualityFn instead of useStore import { useContext } from 'react' import { useStoreWithEqualityFn } from 'zustand/traditional' function useBearContext( selector: (state: BearState) => T, equalityFn?: (left: T, right: T) => boolean, ): T { const store = useContext(BearContext) if (!store) throw new Error('Missing BearContext.Provider in the tree') return useStoreWithEqualityFn(store, selector, equalityFn) } ``` ### Complete example ```tsx // Provider wrapper & custom hook consumer function App2() { return ( ) } ``` ]]>
**Note**: Since Jest and Vitest have slight differences, like Vitest using **ES modules** and Jest using > **CommonJS modules**, you need to keep that in mind if you are using Vitest instead of Jest. The mock provided below will enable the relevant test runner to reset the zustand stores after each test. ### Shared code just for testing purposes This shared code was added to avoid code duplication in our demo since we use the same counter store creator for both implementations, with and without `Context` API — `createStore` and `create`, respectively. ```ts // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) ``` ### Jest In the next steps we are going to setup our Jest environment in order to mock Zustand. ```ts // __mocks__/zustand.ts import { act } from '@testing-library/react' import type * as ZustandExportedTypes from 'zustand' export * from 'zustand' const { create: actualCreate, createStore: actualCreateStore } = jest.requireActual('zustand') // a variable to hold reset functions for all stores declared in the app export const storeResetFns = new Set<() => void>() const createUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreate(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const create = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand create mock') // to support curried version of create return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried }) as typeof ZustandExportedTypes.create const createStoreUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreateStore(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const createStore = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand createStore mock') // to support curried version of createStore return typeof stateCreator === 'function' ? createStoreUncurried(stateCreator) : createStoreUncurried }) as typeof ZustandExportedTypes.createStore // reset all stores after each test run afterEach(() => { act(() => { storeResetFns.forEach((resetFn) => { resetFn() }) }) }) ``` ```ts // setup-jest.ts import '@testing-library/jest-dom' ``` ```ts // jest.config.ts import type { JestConfigWithTsJest } from 'ts-jest' const config: JestConfigWithTsJest = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['./setup-jest.ts'], } export default config ``` > **Note**: to use TypeScript we need to install two packages `ts-jest` and `ts-node`. ### Vitest In the next steps we are going to setup our Vitest environment in order to mock Zustand. > **Warning:** In Vitest you can change the [root](https://vitest.dev/config/#root). > Due to that, you need make sure that you are creating your `__mocks__` directory in the right place. > Let's say that you change the **root** to `./src`, that means you need to create a `__mocks__` > directory under `./src`. The end result would be `./src/__mocks__`, rather than `./__mocks__`. > Creating `__mocks__` directory in the wrong place can lead to issues when using Vitest. ```ts // __mocks__/zustand.ts import { act } from '@testing-library/react' import type * as ZustandExportedTypes from 'zustand' export * from 'zustand' const { create: actualCreate, createStore: actualCreateStore } = await vi.importActual('zustand') // a variable to hold reset functions for all stores declared in the app export const storeResetFns = new Set<() => void>() const createUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreate(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const create = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand create mock') // to support curried version of create return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried }) as typeof ZustandExportedTypes.create const createStoreUncurried = ( stateCreator: ZustandExportedTypes.StateCreator, ) => { const store = actualCreateStore(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } // when creating a store, we get its initial state, create a reset function and add it in the set export const createStore = (( stateCreator: ZustandExportedTypes.StateCreator, ) => { console.log('zustand createStore mock') // to support curried version of createStore return typeof stateCreator === 'function' ? createStoreUncurried(stateCreator) : createStoreUncurried }) as typeof ZustandExportedTypes.createStore // reset all stores after each test run afterEach(() => { act(() => { storeResetFns.forEach((resetFn) => { resetFn() }) }) }) ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we need > to add `import { afterEach, vi } from 'vitest'` at the top. ```ts // global.d.ts /// /// ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we do > need to remove `/// `. ```ts // setup-vitest.ts import '@testing-library/jest-dom/vitest' vi.mock('zustand') // to make it work like Jest (auto-mocking) ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we need > to add `import { vi } from 'vitest'` at the top. ```ts // vitest.config.ts import { defineConfig, mergeConfig } from 'vitest/config' import viteConfig from './vite.config' export default defineConfig((configEnv) => mergeConfig( viteConfig(configEnv), defineConfig({ test: { globals: true, environment: 'jsdom', setupFiles: ['./setup-vitest.ts'], }, }), ), ) ``` ### Testing Components In the next examples we are going to use `useCounterStore` > **Note**: all of these examples are written using TypeScript. ```ts // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) ``` ```ts // stores/use-counter-store.ts import { create } from 'zustand' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const useCounterStore = create()(counterStoreCreator) ``` ```tsx // contexts/use-counter-store-context.tsx import { type ReactNode, createContext, useContext, useState } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const createCounterStore = () => { return createStore(counterStoreCreator) } export type CounterStoreApi = ReturnType export const CounterStoreContext = createContext( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const [store] = useState(() => createCounterStore()) return ( {children} ) } export type UseCounterStoreContextSelector = (store: CounterStore) => T export const useCounterStoreContext = ( selector: UseCounterStoreContextSelector, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (counterStoreContext === undefined) { throw new Error( 'useCounterStoreContext must be used within CounterStoreProvider', ) } return useStoreWithEqualityFn(counterStoreContext, selector, shallow) } ``` ```tsx // components/counter/counter.tsx import { useCounterStore } from '../../stores/use-counter-store' export function Counter() { const { count, inc } = useCounterStore() return (

Counter Store

{count}

) } ``` ```ts // components/counter/index.ts export * from './counter' ``` ```tsx // components/counter/counter.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter } from './counter' describe('Counter', () => { test('should render with initial state of 1', async () => { renderCounter() expect(await screen.findByText(/^1$/)).toBeInTheDocument() expect( await screen.findByRole('button', { name: /one up/i }), ).toBeInTheDocument() }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() renderCounter() expect(await screen.findByText(/^1$/)).toBeInTheDocument() await user.click(await screen.findByRole('button', { name: /one up/i })) expect(await screen.findByText(/^2$/)).toBeInTheDocument() }) }) const renderCounter = () => { return render() } ``` ```tsx // components/counter-with-context/counter-with-context.tsx import { CounterStoreProvider, useCounterStoreContext, } from '../../contexts/use-counter-store-context' const Counter = () => { const { count, inc } = useCounterStoreContext((state) => state) return (

Counter Store Context

{count}

) } export const CounterWithContext = () => { return ( ) } ``` ```tsx // components/counter-with-context/index.ts export * from './counter-with-context' ``` ```tsx // components/counter-with-context/counter-with-context.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { CounterWithContext } from './counter-with-context' describe('CounterWithContext', () => { test('should render with initial state of 1', async () => { renderCounterWithContext() expect(await screen.findByText(/^1$/)).toBeInTheDocument() expect( await screen.findByRole('button', { name: /one up/i }), ).toBeInTheDocument() }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() renderCounterWithContext() expect(await screen.findByText(/^1$/)).toBeInTheDocument() await user.click(await screen.findByRole('button', { name: /one up/i })) expect(await screen.findByText(/^2$/)).toBeInTheDocument() }) }) const renderCounterWithContext = () => { return render() } ``` > **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, we need > to add `import { describe, test, expect } from 'vitest'` at the top of each test file. ### Testing Stores In the next examples we are going to use `useCounterStore` > **Note**: all of these examples are written using TypeScript. ```ts // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) ``` ```ts // stores/use-counter-store.ts import { create } from 'zustand' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const useCounterStore = create()(counterStoreCreator) ``` ```tsx // contexts/use-counter-store-context.tsx import { type ReactNode, createContext, useContext, useState } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const createCounterStore = () => { return createStore(counterStoreCreator) } export type CounterStoreApi = ReturnType export const CounterStoreContext = createContext( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const [store] = useState(() => createCounterStore()) return ( {children} ) } export type UseCounterStoreContextSelector = (store: CounterStore) => T export const useCounterStoreContext = ( selector: UseCounterStoreContextSelector, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (counterStoreContext === undefined) { throw new Error( 'useCounterStoreContext must be used within CounterStoreProvider', ) } return useStoreWithEqualityFn(counterStoreContext, selector, shallow) } ``` ```tsx // components/counter/counter.tsx import { useCounterStore } from '../../stores/use-counter-store' export function Counter() { const { count, inc } = useCounterStore() return (

Counter Store

{count}

) } ``` ```ts // components/counter/index.ts export * from './counter' ``` ```tsx // components/counter/counter.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Counter, useCounterStore } from '../../../stores/use-counter-store.ts' describe('Counter', () => { test('should render with initial state of 1', async () => { renderCounter() expect(useCounterStore.getState().count).toBe(1) }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() renderCounter() expect(useCounterStore.getState().count).toBe(1) await user.click(await screen.findByRole('button', { name: /one up/i })) expect(useCounterStore.getState().count).toBe(2) }) }) const renderCounter = () => { return render() } ``` ```tsx // components/counter-with-context/counter-with-context.tsx import { CounterStoreProvider, useCounterStoreContext, } from '../../contexts/use-counter-store-context' const Counter = () => { const { count, inc } = useCounterStoreContext((state) => state) return (

Counter Store Context

{count}

) } export const CounterWithContext = () => { return ( ) } ``` ```tsx // components/counter-with-context/index.ts export * from './counter-with-context' ``` ```tsx // components/counter-with-context/counter-with-context.test.tsx import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { CounterStoreContext } from '../../../contexts/use-counter-store-context' import { counterStoreCreator } from '../../../shared/counter-store-creator' describe('CounterWithContext', () => { test('should render with initial state of 1', async () => { const counterStore = counterStoreCreator() renderCounterWithContext(counterStore) expect(counterStore.getState().count).toBe(1) expect( await screen.findByRole('button', { name: /one up/i }), ).toBeInTheDocument() }) test('should increase count by clicking a button', async () => { const user = userEvent.setup() const counterStore = counterStoreCreator() renderCounterWithContext(counterStore) expect(counterStore.getState().count).toBe(1) await user.click(await screen.findByRole('button', { name: /one up/i })) expect(counterStore.getState().count).toBe(2) }) }) const renderCounterWithContext = (store) => { return render(, { wrapper: ({ children }) => ( {children} ), }) } ``` ## References - **React Testing Library**: [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro) is a very lightweight solution for testing React components. It provides utility functions on top of `react-dom` and `react-dom/test-utils`, in a way that encourages better testing practices. Its primary guiding principle is: "The more your tests resemble the way your software is used, the more confidence they can give you." - **Native Testing Library**: [Native Testing Library (RNTL)](https://testing-library.com/docs/react-native-testing-library/intro) is a very lightweight solution for testing React Native components, similarly to RTL, but its functions are built on top of `react-test-renderer`. - **Testing Implementation Details**: Blog post by Kent C. Dodds on why he recommends to avoid [testing implementation details](https://kentcdodds.com/blog/testing-implementation-details). ## Demos - Jest: https://stackblitz.com/edit/jest-zustand - Vitest: https://stackblitz.com/edit/vitest-zustand ]]>
({ storeSliceA: ..., storeSliceB: ..., storeSliceC: ..., updateX: () => set(...), updateY: () => set(...), })) ``` ## Redux-like patterns If you can't live without Redux-like reducers, you can define a `dispatch` function on the root level of the store: ```typescript const types = { increase: 'INCREASE', decrease: 'DECREASE' } const reducer = (state, { type, by = 1 }) => { switch (type) { case types.increase: return { grumpiness: state.grumpiness + by } case types.decrease: return { grumpiness: state.grumpiness - by } } } const useGrumpyStore = create((set) => ({ grumpiness: 0, dispatch: (args) => set((state) => reducer(state, args)), })) const dispatch = useGrumpyStore((state) => state.dispatch) dispatch({ type: types.increase, by: 2 }) ``` You could also use our redux-middleware. It wires up your main reducer, sets initial state, and adds a dispatch function to the state itself and the vanilla api. ```typescript import { redux } from 'zustand/middleware' const useReduxStore = create(redux(reducer, initialState)) ``` Another way to update the store could be through functions wrapping the state functions. These could also handle side-effects of actions. For example, with HTTP-calls. To use Zustand in a non-reactive way, see [the readme](https://github.com/pmndrs/zustand#readingwriting-state-and-reacting-to-changes-outside-of-components). ]]> ()((set, get, store) => ({ // your code here reset: () => { set(store.getInitialState()) }, })) ``` Resetting multiple stores at once ```ts import type { StateCreator } from 'zustand' import { create: actualCreate } from 'zustand' const storeResetFns = new Set<() => void>() const resetAllStores = () => { storeResetFns.forEach((resetFn) => { resetFn() }) } export const create = (() => { return (stateCreator: StateCreator) => { const store = actualCreate(stateCreator) storeResetFns.add(() => { store.setState(store.getInitialState(), true) }) return store } }) as typeof actualCreate ``` ## Demo - Basic: https://stackblitz.com/edit/zustand-how-to-reset-state-basic - Advanced: https://stackblitz.com/edit/zustand-how-to-reset-state-advanced ]]> ()(stateCreatorFn: StateCreator): UseBoundStore> ``` ## Reference ### `create(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `create` returns a React Hook with API utilities, `setState`, `getState`, `getInitialState` and `subscribe`, attached. It lets you return data that is based on current state, using a selector function. It should take a selector function as its only argument. ## Usage ### Updating state based on previous state To update a state based on previous state we should use **updater functions**. Read more about that [here](https://react.dev/learn/queueing-a-series-of-state-updates). This example shows how you can support **updater functions** within **actions**. ```tsx import { create } from 'zustand' type AgeStoreState = { age: number } type AgeStoreActions = { setAge: ( nextAge: | AgeStoreState['age'] | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), ) => void } type AgeStore = AgeStoreState & AgeStoreActions const useAgeStore = create()((set) => ({ age: 42, setAge: (nextAge) => { set((state) => ({ age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, })) }, })) export default function App() { const age = useAgeStore((state) => state.age) const setAge = useAgeStore((state) => state.setAge) function increment() { setAge((currentAge) => currentAge + 1) } return ( <>

Your age: {age}

) } ``` ### Updating Primitives in State State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. > [!NOTE] > By default, `set` function performs a shallow merge. If you need to completely replace the state > with a new one, use the `replace` parameter set to `true` ```tsx import { create } from 'zustand' type XStore = number const useXStore = create()(() => 0) export default function MovingDot() { const x = useXStore() const setX = (nextX: number) => { useXStore.setState(nextX, true) } const position = { y: 0, x } return (
{ setX(e.clientX) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Objects in State Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object. By default, `set` function performs a shallow merge. For most updates where you only need to modify specific properties, the default shallow merge is preferred as it's more efficient. To completely replace the state with a new one, use the `replace` parameter set to `true` with caution, as it discards any existing nested data within the state. ```tsx import { create } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = create()((set) => ({ position: { x: 0, y: 0 }, setPosition: (nextPosition) => set({ position: nextPosition }), })) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Arrays in State Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. By default, `set` function performs a shallow merge. To update array values we should assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely replace the state with a new one, use the `replace` parameter set to `true`. > [!IMPORTANT] > We should prefer immutable operations like: `[...array]`, `concat(...)`, `filter(...)`, > `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid > mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`, > `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`. ```tsx import { create } from 'zustand' type PositionStore = [number, number] const usePositionStore = create()(() => [0, 0]) export default function MovingDot() { const [x, y] = usePositionStore() const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition, true) } const position = { x, y } return (
{ setPosition([e.clientX, e.clientY]) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating state with no store actions Defining actions at module level, external to the store have a few advantages like: it doesn't require a hook to call an action, and it facilitates code splitting. > [!NOTE] > The recommended way is to colocate actions and states within the store (let your actions be > located together with your state). ```tsx import { create } from 'zustand' const usePositionStore = create<{ x: number y: number }>()(() => ({ x: 0, y: 0 })) const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition) } export default function MovingDot() { const position = usePositionStore() return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ### Subscribing to state updates By subscribing to state updates, you register a callback that fires whenever the store's state updates. We can use `subscribe` for external state management. ```tsx import { useEffect } from 'react' import { create } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = create()((set) => ({ position: { x: 0, y: 0 }, setPosition: (nextPosition) => set({ position: nextPosition }), })) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) useEffect(() => { const unsubscribePositionStore = usePositionStore.subscribe( ({ position }) => { console.log('new position', { position }) }, ) return () => { unsubscribePositionStore() } }, []) return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ## Troubleshooting ### I’ve updated the state, but the screen doesn’t update In the previous example, the `position` object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields. These input fields don’t work because the `onChange` handlers mutate the state: ```tsx import { create } from 'zustand' type PersonStoreState = { firstName: string lastName: string email: string } type PersonStoreActions = { setPerson: (nextPerson: Partial) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = create()((set) => ({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', setPerson: (nextPerson) => set(nextPerson), })) export default function Form() { const person = usePersonStore((state) => state) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { person.firstName = e.target.value } function handleLastNameChange(e: ChangeEvent) { person.lastName = e.target.value } function handleEmailChange(e: ChangeEvent) { person.email = e.target.value } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` For example, this line mutates the state from a past render: ```tsx person.firstName = e.target.value ``` The reliable way to get the behavior you’re looking for is to create a new object and pass it to `setPerson`. But here you want to also copy the existing data into it because only one of the fields has changed: ```ts setPerson({ ...person, firstName: e.target.value }) // New first name from the input ``` > [!NOTE] > We don’t need to copy every property separately due to `set` function performing shallow merge by > default. Now the form works! Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly! ```tsx {27,31,35} import { create } from 'zustand' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = create()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set(nextPerson), })) export default function Form() { const person = usePersonStore((state) => state.person) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { setPerson({ ...person, firstName: e.target.value }) } function handleLastNameChange(e: ChangeEvent) { setPerson({ ...person, lastName: e.target.value }) } function handleEmailChange(e: ChangeEvent) { setPerson({ ...person, email: e.target.value }) } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` ]]> ()(stateCreatorFn: StateCreator): StoreApi ``` ## Reference ### `createStore(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `createStore` returns a vanilla store that exposes API utilities, `setState`, `getState`, `getInitialState` and `subscribe`. ## Usage ### Updating state based on previous state This example shows how you can support **updater functions** within **actions**. ```tsx import { createStore } from 'zustand/vanilla' type AgeStoreState = { age: number } type AgeStoreActions = { setAge: ( nextAge: | AgeStoreState['age'] | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), ) => void } type AgeStore = AgeStoreState & AgeStoreActions const ageStore = createStore()((set) => ({ age: 42, setAge: (nextAge) => set((state) => ({ age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, })), })) function increment() { ageStore.getState().setAge((currentAge) => currentAge + 1) } const $yourAgeHeading = document.getElementById( 'your-age', ) as HTMLHeadingElement const $incrementBy3Button = document.getElementById( 'increment-by-3', ) as HTMLButtonElement const $incrementBy1Button = document.getElementById( 'increment-by-1', ) as HTMLButtonElement $incrementBy3Button.addEventListener('click', () => { increment() increment() increment() }) $incrementBy1Button.addEventListener('click', () => { increment() }) const render: Parameters[0] = (state) => { $yourAgeHeading.innerHTML = `Your age: ${state.age}` } render(ageStore.getInitialState(), ageStore.getInitialState()) ageStore.subscribe(render) ``` Here's the `html` code ```html

``` ### Updating Primitives in State State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. > [!NOTE] > By default, `set` function performs a shallow merge. If you need to completely replace > the state with a new one, use the `replace` parameter set to `true` ```ts import { createStore } from 'zustand/vanilla' type XStore = number const xStore = createStore()(() => 0) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { xStore.setState(event.clientX, true) }) const render: Parameters[0] = (x) => { $dot.style.transform = `translate(${x}px, 0)` } render(xStore.getInitialState(), xStore.getInitialState()) xStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Updating Objects in State Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object. By default, `set` function performs a shallow merge. For most updates where you only need to modify specific properties, the default shallow merge is preferred as it's more efficient. To completely replace the state with a new one, use the `replace` parameter set to `true` with caution, as it discards any existing nested data within the state. ```ts import { createStore } from 'zustand/vanilla' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Updating Arrays in State Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. By default, `set` function performs a shallow merge. To update array values we should assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely replace the state with a new one, use the `replace` parameter set to `true`. > [!IMPORTANT] > We should prefer immutable operations like: `[...array]`, `concat(...)`, `filter(...)`, > `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid > mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`, > `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`. ```ts import { createStore } from 'zustand/vanilla' type PositionStore = [number, number] const positionStore = createStore()(() => [0, 0]) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.setState([event.clientX, event.clientY], true) }) const render: Parameters[0] = ([x, y]) => { $dot.style.transform = `translate(${x}px, ${y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Subscribing to state updates By subscribing to state updates, you register a callback that fires whenever the store's state updates. We can use `subscribe` for external state management. ```ts import { createStore } from 'zustand/vanilla' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) const $dot = document.getElementById('dot') as HTMLDivElement $dot.addEventListener('mouseenter', (event) => { const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight positionStore.getState().setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) const logger: Parameters[0] = (state) => { console.log('new position', { position: state.position }) } positionStore.subscribe(logger) ``` Here's the `html` code ```html
``` ## Troubleshooting ### I’ve updated the state, but the screen doesn’t update In the previous example, the `position` object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields. These input fields don’t work because the `oninput` handlers mutate the state: ```ts import { createStore } from 'zustand/vanilla' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (person) => set({ person }), })) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().person.firstName = (event.target as any).value } function handleLastNameChange(event: Event) { personStore.getState().person.lastName = (event.target as any).value } function handleEmailChange(event: Event) { personStore.getState().person.email = (event.target as any).value } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` Here's the `html` code ```html

``` For example, this line mutates the state from a past render: ```ts personStore.getState().firstName = (e.target as any).value ``` The reliable way to get the behavior you’re looking for is to create a new object and pass it to `setPerson`. But here you want to also copy the existing data into it because only one of the fields has changed: ```ts personStore.getState().setPerson({ firstName: e.target.value, // New first name from the input }) ``` > [!NOTE] > We don’t need to copy every property separately due to `set` function performing shallow merge by > default. Now the form works! Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly! ```ts {32-34,38-40,44-46} import { createStore } from 'zustand/vanilla' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (person) => set({ person }), })) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().setPerson({ ...personStore.getState().person, firstName: (event.target as any).value, }) } function handleLastNameChange(event: Event) { personStore.getState().setPerson({ ...personStore.getState().person, lastName: (event.target as any).value, }) } function handleEmailChange(event: Event) { personStore.getState().setPerson({ ...personStore.getState().person, email: (event.target as any).value, }) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` ]]>
[!IMPORTANT] > In order to use `createWithEqualityFn` from `zustand/traditional` you need to install > `use-sync-external-store` library due to `zustand/traditional` relies on `useSyncExternalStoreWithSelector`. ```js const useSomeStore = createWithEqualityFn(stateCreatorFn, equalityFn) ``` - [Types](#types) - [Signature](#createwithequalityfn-signature) - [Reference](#reference) - [Usage](#usage) - [Updating state based on previous state](#updating-state-based-on-previous-state) - [Updating Primitives in State](#updating-primitives-in-state) - [Updating Objects in State](#updating-objects-in-state) - [Updating Arrays in State](#updating-arrays-in-state) - [Updating state with no store actions](#updating-state-with-no-store-actions) - [Subscribing to state updates](#subscribing-to-state-updates) - [Troubleshooting](#troubleshooting) - [I’ve updated the state, but the screen doesn’t update](#ive-updated-the-state-but-the-screen-doesnt-update) ## Types ### Signature ```ts createWithEqualityFn()(stateCreatorFn: StateCreator, equalityFn?: (a: T, b: T) => boolean): UseBoundStore> ``` ## Reference ### `createWithEqualityFn(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. - **optional** `equalityFn`: Defaults to `Object.is`. A function that lets you skip re-renders. #### Returns `createWithEqualityFn` returns a React Hook with API utilities attached, just like `create`. It lets you return data that is based on current state, using a selector function, and lets you skip re-renders using an equality function. It should take a selector function, and an equality function as arguments. ## Usage ### Updating state based on previous state To update a state based on previous state we should use **updater functions**. Read more about that [here](https://react.dev/learn/queueing-a-series-of-state-updates). This example shows how you can support **updater functions** within **actions**. ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type AgeStoreState = { age: number } type AgeStoreActions = { setAge: ( nextAge: | AgeStoreState['age'] | ((currentAge: AgeStoreState['age']) => AgeStoreState['age']), ) => void } type AgeStore = AgeStoreState & AgeStoreActions const useAgeStore = createWithEqualityFn()( (set) => ({ age: 42, setAge: (nextAge) => set((state) => ({ age: typeof nextAge === 'function' ? nextAge(state.age) : nextAge, })), }), shallow, ) export default function App() { const age = useAgeStore((state) => state.age) const setAge = useAgeStore((state) => state.setAge) function increment() { setAge((currentAge) => currentAge + 1) } return ( <>

Your age: {age}

) } ``` ### Updating Primitives in State State can hold any kind of JavaScript value. When you want to update built-in primitive values like numbers, strings, booleans, etc. you should directly assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. > [!NOTE] > By default, `set` function performs a shallow merge. If you need to completely replace > the state with a new one, use the `replace` parameter set to `true` ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type XStore = number const useXStore = createWithEqualityFn()(() => 0, shallow) export default function MovingDot() { const x = useXStore() const setX = (nextX: number) => { useXStore.setState(nextX, true) } const position = { y: 0, x } return (
{ setX(e.clientX) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Objects in State Objects are **mutable** in JavaScript, but you should treat them as **immutable** when you store them in state. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use the new object. By default, `set` function performs a shallow merge. For most updates where you only need to modify specific properties, the default shallow merge is preferred as it's more efficient. To completely replace the state with a new one, use the `replace` parameter set to `true` with caution, as it discards any existing nested data within the state. ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = createWithEqualityFn()( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), shallow, ) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating Arrays in State Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. By default, `set` function performs a shallow merge. To update array values we should assign new values to ensure updates are applied correctly, and avoid unexpected behaviors. To completely replace the state with a new one, use the `replace` parameter set to `true`. > [!IMPORTANT] > We should prefer immutable operations like: `[...array]`, `concat(...)`, `filter(...)`, > `slice(...)`, `map(...)`, `toSpliced(...)`, `toSorted(...)`, and `toReversed(...)`, and avoid > mutable operations like `array[arrayIndex] = ...`, `push(...)`, `unshift(...)`, `pop(...)`, > `shift(...)`, `splice(...)`, `reverse(...)`, and `sort(...)`. ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PositionStore = [number, number] const usePositionStore = createWithEqualityFn()( () => [0, 0], shallow, ) export default function MovingDot() { const [x, y] = usePositionStore() const position = { x, y } const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition, true) } return (
{ setPosition([e.clientX, e.clientY]) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` ### Updating state with no store actions Defining actions at module level, external to the store have a few advantages like: it doesn't require a hook to call an action, and it facilitates code splitting. > [!NOTE] > The recommended way is to colocate actions and states within the store (let your actions be > located together with your state). ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' const usePositionStore = createWithEqualityFn<{ x: number y: number }>()(() => ({ x: 0, y: 0 }), shallow) const setPosition: typeof usePositionStore.setState = (nextPosition) => { usePositionStore.setState(nextPosition) } export default function MovingDot() { const position = usePositionStore() return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ### Subscribing to state updates By subscribing to state updates, you register a callback that fires whenever the store's state updates. We can use `subscribe` for external state management. ```tsx import { useEffect } from 'react' import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const usePositionStore = createWithEqualityFn()( (set) => ({ position: { x: 0, y: 0 }, setPosition: (nextPosition) => set({ position: nextPosition }), }), shallow, ) export default function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) useEffect(() => { const unsubscribePositionStore = usePositionStore.subscribe( ({ position }) => { console.log('new position', { position }) }, ) return () => { unsubscribePositionStore() } }, []) return (
{ const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }} />
) } ``` ## Troubleshooting ### I’ve updated the state, but the screen doesn’t update In the previous example, the `position` object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields. These input fields don’t work because the `onChange` handlers mutate the state: ```tsx import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = createWithEqualityFn()( (set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (person) => set({ person }), }), shallow, ) export default function Form() { const person = usePersonStore((state) => state.person) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { person.firstName = e.target.value } function handleLastNameChange(e: ChangeEvent) { person.lastName = e.target.value } function handleEmailChange(e: ChangeEvent) { person.email = e.target.value } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` For example, this line mutates the state from a past render: ```tsx person.firstName = e.target.value ``` The reliable way to get the behavior you’re looking for is to create a new object and pass it to `setPerson`. But here you want to also copy the existing data into it because only one of the fields has changed: ```ts setPerson({ ...person, firstName: e.target.value }) // New first name from the input ``` > [!NOTE] > We don’t need to copy every property separately due to `set` function performing shallow merge by > default. Now the form works! Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly! ```tsx {32,36,40} import { type ChangeEvent } from 'react' import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: (nextPerson: PersonStoreState['person']) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = createWithEqualityFn()( (set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set({ person: nextPerson }), }), shallow, ) export default function Form() { const person = usePersonStore((state) => state.person) const setPerson = usePersonStore((state) => state.setPerson) function handleFirstNameChange(e: ChangeEvent) { setPerson({ ...person, firstName: e.target.value }) } function handleLastNameChange(e: ChangeEvent) { setPerson({ ...person, lastName: e.target.value }) } function handleEmailChange(e: ChangeEvent) { setPerson({ ...person, email: e.target.value }) } return ( <>

{person.firstName} {person.lastName} ({person.email})

) } ``` ]]> [!NOTE] > Shallow lets you perform quick comparisons, but keep its limitations in mind. ```js const equal = shallow(a, b) ``` - [Types](#types) - [Signature](#shallow-signature) - [Reference](#reference) - [Usage](#usage) - [Comparing Primitives](#comparing-primitives) - [Comparing Objects](#comparing-objects) - [Comparing Sets](#comparing-sets) - [Comparing Maps](#comparing-maps) - [Troubleshooting](#troubleshooting) - [Comparing objects returns `false` even if they are identical.](#comparing-objects-returns-false-even-if-they-are-identical) - [Comparing objects with different prototypes](#comparing-objects-with-different-prototypes) ## Types ### Signature ```ts shallow(a: T, b: T): boolean ``` ## Reference ### `shallow(a, b)` #### Parameters - `a`: The first value. - `b`: The second value. #### Returns `shallow` returns `true` when `a` and `b` are equal based on a shallow comparison of their **top-level** properties. Otherwise, it should return `false`. ## Usage ### Comparing Primitives When comparing primitive values like `string`s, `number`s, `boolean`s, and `BigInt`s, both `Object.is` and `shallow` function return `true` if the values are the same. This is because primitive values are compared by their actual value rather than by reference. ```ts const stringLeft = 'John Doe' const stringRight = 'John Doe' Object.is(stringLeft, stringRight) // -> true shallow(stringLeft, stringRight) // -> true const numberLeft = 10 const numberRight = 10 Object.is(numberLeft, numberRight) // -> true shallow(numberLeft, numberRight) // -> true const booleanLeft = true const booleanRight = true Object.is(booleanLeft, booleanRight) // -> true shallow(booleanLeft, booleanRight) // -> true const bigIntLeft = 1n const bigIntRight = 1n Object.is(bigIntLeft, bigIntRight) // -> true shallow(bigIntLeft, bigIntRight) // -> true ``` ### Comparing Objects When comparing objects, it's important to understand how `Object.is` and `shallow` function operate, as they handle comparisons differently. The `shallow` function returns `true` because shallow performs a shallow comparison of the objects. It checks if the top-level properties and their values are the same. In this case, the top-level properties (`firstName`, `lastName`, and `age`) and their values are identical between `objectLeft` and `objectRight`, so shallow considers them equal. ```ts const objectLeft = { firstName: 'John', lastName: 'Doe', age: 30, } const objectRight = { firstName: 'John', lastName: 'Doe', age: 30, } Object.is(objectLeft, objectRight) // -> false shallow(objectLeft, objectRight) // -> true ``` ### Comparing Sets When comparing sets, it's important to understand how `Object.is` and `shallow` function operate, as they handle comparisons differently. The `shallow` function returns `true` because shallow performs a shallow comparison of the sets. It checks if the top-level properties (in this case, the sets themselves) are the same. Since `setLeft` and `setRight` are both instances of the Set object and contain the same elements, shallow considers them equal. ```ts const setLeft = new Set([1, 2, 3]) const setRight = new Set([1, 2, 3]) Object.is(setLeft, setRight) // -> false shallow(setLeft, setRight) // -> true ``` ### Comparing Maps When comparing maps, it's important to understand how `Object.is` and `shallow` function operate, as they handle comparisons differently. The `shallow` returns `true` because shallow performs a shallow comparison of the maps. It checks if the top-level properties (in this case, the maps themselves) are the same. Since `mapLeft` and `mapRight` are both instances of the Map object and contain the same key-value pairs, shallow considers them equal. ```ts const mapLeft = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]) const mapRight = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]) Object.is(mapLeft, mapRight) // -> false shallow(mapLeft, mapRight) // -> true ``` ## Troubleshooting ### Comparing objects returns `false` even if they are identical. The `shallow` function performs a shallow comparison. A shallow comparison checks if the top-level properties of two objects are equal. It does not check nested objects or deeply nested properties. In other words, it only compares the references of the properties. In the following example, the shallow function returns `false` because it compares only the top-level properties and their references. The address property in both objects is a nested object, and even though their contents are identical, their references are different. Consequently, shallow sees them as different, resulting in `false`. ```ts const objectLeft = { firstName: 'John', lastName: 'Doe', age: 30, address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: { lat: '-37.3159', lng: '81.1496', }, }, } const objectRight = { firstName: 'John', lastName: 'Doe', age: 30, address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: { lat: '-37.3159', lng: '81.1496', }, }, } Object.is(objectLeft, objectRight) // -> false shallow(objectLeft, objectRight) // -> false ``` If we remove the `address` property, the shallow comparison would work as expected because all top-level properties would be primitive values or references to the same values: ```ts const objectLeft = { firstName: 'John', lastName: 'Doe', age: 30, } const objectRight = { firstName: 'John', lastName: 'Doe', age: 30, } Object.is(objectLeft, objectRight) // -> false shallow(objectLeft, objectRight) // -> true ``` In this modified example, `objectLeft` and `objectRight` have the same top-level properties and primitive values. Since `shallow` function only compares the top-level properties, it will return `true` because the primitive values (`firstName`, `lastName`, and `age`) are identical in both objects. ### Comparing objects with different prototypes The `shallow` function checks whether the two objects have the same prototype. If their prototypes are referentially different, shallow will return `false`. This comparison is done using: ```ts Object.getPrototypeOf(a) === Object.getPrototypeOf(b) ``` > [!IMPORTANT] > Objects created with the object initializer (`{}`) or with `new Object()` inherit from > `Object.prototype` by default. However, objects created with `Object.create(proto)` inherit from > the proto you pass in—which may not be `Object.prototype.` ```ts const a = Object.create({}) // -> prototype is `{}` const b = {} // -> prototype is `Object.prototype` shallow(a, b) // -> false ``` ]]> , U = T>(store: StoreApi, selectorFn?: (state: T) => U) => UseBoundStore> ``` ## Reference ### `useStore(store, selectorFn)` #### Parameters - `storeApi`: The instance that lets you access to store API utilities. - `selectorFn`: A function that lets you return data that is based on current state. #### Returns `useStore` returns any data based on current state depending on the selector function. It should take a store, and a selector function as arguments. ## Usage ### Using a global vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) ``` Next, we'll create a `MovingDot` component that renders a div representing the dot. This component will use the store to track and update the dot's position. ```tsx function MovingDot() { const position = useStore(positionStore, (state) => state.position) const setPosition = useStore(positionStore, (state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` Finally, we’ll render the `MovingDot` component in our `App` component. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { createStore, useStore } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) function MovingDot() { const position = useStore(positionStore, (state) => state.position) const setPosition = useStore(positionStore, (state) => state.setPosition) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } export default function App() { return } ``` ### Using dynamic global vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStore( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), ) return (
Content of Tab {currentTabIndex + 1}

) ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { useState } from 'react' import { createStore, useStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) export default function App() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStore( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), ) return (
Content of Tab {currentTabIndex + 1}

) } ``` ### Using scoped (non-global) vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } ``` Next, we'll create a context and a provider component to pass down the store through the React component tree. This allows each `MovingDot` component to have its own independent state. ```tsx const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } ``` To simplify accessing the store, we’ll create a React custom hook, `usePositionStore`. This hook will read the store from the context and allow us to select specific parts of the state. ```ts function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStore(store, selector) } ``` Now, let's create the `MovingDot` component, which will render a dot that follows the mouse cursor within its container. ```tsx function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } ``` Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` components, each with its own independent state. ```tsx export default function App() { return (
) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, createContext, useContext } from 'react' import { createStore, useStore } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStore(store, selector) } function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } export default function App() { return (
) } ``` ### Using dynamic scoped (non-global) vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts import { createStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } ``` Next, we need a way to manage and access these stores throughout our app. We’ll use React’s context for this. ```tsx const CounterStoresContext = createContext(null) const CounterStoresProvider = ({ children }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } ``` Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a given tab. ```tsx const useCounterStore = ( currentTabIndex: number, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( () => createCounterStoreFactory(stores), [stores], ) return useStore(getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`)) } ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return ( ) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, useCallback, useContext, createContext, } from 'react' import { createStore, useStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const CounterStoresContext = createContext > | null>(null) const CounterStoresProvider = ({ children }: { children: ReactNode }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } const useCounterStore = ( key: string, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( (key: string) => createCounterStoreFactory(stores!)(key), [stores], ) return useStore(getOrCreateCounterStoreByKey(key), selector) } function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } export default function App() { return ( ) } ``` ## Troubleshooting TBD ]]> [!IMPORTANT] > In order to use `useStoreWithEqualityFn` from `zustand/traditional` you need to install > `use-sync-external-store` library due to `zustand/traditional` relies on `useSyncExternalStoreWithSelector`. ```js const someState = useStoreWithEqualityFn(store, selectorFn, equalityFn) ``` - [Types](#types) - [Signature](#signature) - [Reference](#reference) - [Usage](#usage) - [Using a global vanilla store in React](#using-a-global-vanilla-store-in-react) - [Using dynamic vanilla stores in React](#using-dynamic-global-vanilla-stores-in-react) - [Using scoped (non-global) vanilla store in React](#using-scoped-non-global-vanilla-store-in-react) - [Using dynamic scoped (non-global) vanilla stores in React](#using-dynamic-scoped-non-global-vanilla-stores-in-react) - [Troubleshooting](#troubleshooting) ### Signature ```ts useStoreWithEqualityFn(store: StoreApi, selectorFn: (state: T) => U, equalityFn?: (a: T, b: T) => boolean): U ``` ## Reference ### `useStoreWithEqualityFn(store, selectorFn, equalityFn)` #### Parameters - `storeApi`: The instance that lets you access to store API utilities. - `selectorFn`: A function that lets you return data that is based on current state. - `equalityFn`: A function that lets you skip re-renders. #### Returns `useStoreWithEqualityFn` returns any data based on current state depending on the selector function, and lets you skip re-renders using an equality function. It should take a store, a selector function, and an equality function as arguments. ## Usage ### Using a global vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx import { createStore, useStore } from 'zustand' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) ``` Next, we'll create a `MovingDot` component that renders a div representing the dot. This component will use the store to track and update the dot's position. ```tsx function MovingDot() { const position = useStoreWithEqualityFn( positionStore, (state) => state.position, shallow, ) const setPosition = useStoreWithEqualityFn( positionStore, (state) => state.setPosition, shallow, ) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } ``` Finally, we’ll render the `MovingDot` component in our `App` component. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) function MovingDot() { const position = useStoreWithEqualityFn( positionStore, (state) => state.position, shallow, ) const setPosition = useStoreWithEqualityFn( positionStore, (state) => state.setPosition, shallow, ) return (
{ setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} >
) } export default function App() { return } ``` ### Using dynamic global vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts import { createStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStoreWithEqualityFn( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), (state) => state, shallow, ) return (
Content of Tab {currentTabIndex + 1}

) ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return } ``` Here is what the code should look like: ```tsx import { useState } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const defaultCounterStores = new Map< string, ReturnType >() const createCounterStoreFactory = ( counterStores: typeof defaultCounterStores, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const getOrCreateCounterStoreByKey = createCounterStoreFactory(defaultCounterStores) export default function App() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStoreWithEqualityFn( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), (state) => state, shallow, ) return (
Content of Tab {currentTabIndex + 1}

) } ``` ### Using scoped (non-global) vanilla store in React First, let's set up a store that will hold the position of the dot on the screen. We'll define the store to manage `x` and `y` coordinates and provide an action to update these coordinates. ```tsx type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } ``` Next, we'll create a context and a provider component to pass down the store through the React component tree. This allows each `MovingDot` component to have its own independent state. ```tsx const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } ``` To simplify accessing the store, we’ll create a React custom hook, `usePositionStore`. This hook will read the store from the context and allow us to select specific parts of the state. ```ts function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStoreWithEqualityFn(store, selector, shallow) } ``` Now, let's create the `MovingDot` component, which will render a dot that follows the mouse cursor within its container. ```tsx function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } ``` Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` components, each with its own independent state. ```tsx export default function App() { return (
) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, createContext, useContext } from 'react' import { createStore } from 'zustand' import { useStoreWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/shallow' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (store === null) { throw new Error( 'usePositionStore must be used within PositionStoreProvider', ) } return useStoreWithEqualityFn(store, selector, shallow) } function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
{ setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} >
) } export default function App() { return (
) } ``` ### Using dynamic scoped (non-global) vanilla stores in React First, we'll create a factory function that generates a store for managing the counter state. Each tab will have its own instance of this store. ```ts type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } ``` Next, we'll create a factory function that manages the creation and retrieval of counter stores. This allows each tab to have its own independent counter. ```ts const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } ``` Next, we need a way to manage and access these stores throughout our app. We’ll use React’s context for this. ```tsx const CounterStoresContext = createContext(null) const CounterStoresProvider = ({ children }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } ``` Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a given tab. ```tsx const useCounterStore = ( key: string, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( (key: string) => createCounterStoreFactory(stores!)(key), [stores], ) return useStore(getOrCreateCounterStoreByKey(key), selector) } ``` Now, let’s build the Tabs component, where users can switch between tabs and increment each tab’s counter. ```tsx function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } ``` Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. ```tsx export default function App() { return ( ) } ``` Here is what the code should look like: ```tsx import { type ReactNode, useState, useCallback, useContext, createContext, } from 'react' import { createStore, useStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const createCounterStoreFactory = ( counterStores: Map>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const CounterStoresContext = createContext > | null>(null) const CounterStoresProvider = ({ children }: { children: ReactNode }) => { const [stores] = useState( () => new Map>(), ) return ( {children} ) } const useCounterStore = ( key: string, selector: (state: CounterStore) => U, ) => { const stores = useContext(CounterStoresContext) if (stores === undefined) { throw new Error('useCounterStore must be used within CounterStoresProvider') } const getOrCreateCounterStoreByKey = useCallback( (key: string) => createCounterStoreFactory(stores!)(key), [stores], ) return useStore(getOrCreateCounterStoreByKey(key), selector) } function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return (
Content of Tab {currentTabIndex + 1}

) } export default function App() { return ( ) } ``` ## Troubleshooting TBD ]]> (selectorFn: (state: T) => U): (state: T) => U ``` ## Reference ### `useShallow(selectorFn)` #### Parameters - `selectorFn`: A function that lets you return data that is based on current state. #### Returns `useShallow` returns a memoized version of a selector function using a shallow comparison for memoization. ## Usage ### Writing a memoized selector First, we need to setup a store to hold the state for the bear family. In this store, we define three properties: `papaBear`, `mamaBear`, and `babyBear`, each representing a different member of the bear family and their respective oatmeal pot sizes. ```tsx import { create } from 'zustand' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', babyBear: 'A little, small, wee pot', })) ``` Next, we'll create a `BearNames` component that retrieves the keys of our state (the bear family members) and displays them. ```tsx function BearNames() { const names = useBearFamilyMealsStore((state) => Object.keys(state)) return
{names.join(', ')}
} ``` Next, we will create a `UpdateBabyBearMeal` component that periodically updates baby bear's meal choice. ```tsx const meals = [ 'A tiny, little, wee bowl', 'A small, petite, tiny pot', 'A wee, itty-bitty, small bowl', 'A little, petite, tiny dish', 'A tiny, small, wee vessel', 'A small, little, wee cauldron', 'A little, tiny, small cup', 'A wee, small, little jar', 'A tiny, wee, small pan', 'A small, wee, little crock', ] function UpdateBabyBearMeal() { useEffect(() => { const timer = setInterval(() => { useBearFamilyMealsStore.setState({ babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], }) }, 1000) return () => { clearInterval(timer) } }, []) return null } ``` Finally, we combine both components in the `App` component to see them in action. ```tsx export default function App() { return ( <> ) } ``` Here is what the code should look like: ```tsx import { useEffect } from 'react' import { create } from 'zustand' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', babyBear: 'A little, small, wee pot', })) const meals = [ 'A tiny, little, wee bowl', 'A small, petite, tiny pot', 'A wee, itty-bitty, small bowl', 'A little, petite, tiny dish', 'A tiny, small, wee vessel', 'A small, little, wee cauldron', 'A little, tiny, small cup', 'A wee, small, little jar', 'A tiny, wee, small pan', 'A small, wee, little crock', ] function UpdateBabyBearMeal() { useEffect(() => { const timer = setInterval(() => { useBearFamilyMealsStore.setState({ babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], }) }, 1000) return () => { clearInterval(timer) } }, []) return null } function BearNames() { const names = useBearFamilyMealsStore((state) => Object.keys(state)) return
{names.join(', ')}
} export default function App() { return ( <> ) } ``` Everything might look fine, but there’s a small problem: the `BearNames` component keeps re-rendering even if the names haven’t changed. This happens because the component re-renders whenever any part of the state changes, even if the specific part we care about (the list of names) hasn’t changed. To fix this, we use `useShallow` to make sure the component only re-renders when the actual keys of the state change: ```tsx function BearNames() { const names = useBearFamilyMealsStore( useShallow((state) => Object.keys(state)), ) return
{names.join(', ')}
} ``` Here is what the code should look like: ```tsx import { useEffect } from 'react' import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create()(() => ({ papaBear: 'large porridge-pot', mamaBear: 'middle-size porridge pot', babyBear: 'A little, small, wee pot', })) const meals = [ 'A tiny, little, wee bowl', 'A small, petite, tiny pot', 'A wee, itty-bitty, small bowl', 'A little, petite, tiny dish', 'A tiny, small, wee vessel', 'A small, little, wee cauldron', 'A little, tiny, small cup', 'A wee, small, little jar', 'A tiny, wee, small pan', 'A small, wee, little crock', ] function UpdateBabyBearMeal() { useEffect(() => { const timer = setInterval(() => { useBearFamilyMealsStore.setState({ babyBear: meals[Math.floor(Math.random() * (meals.length - 1))], }) }, 1000) return () => { clearInterval(timer) } }, []) return null } function BearNames() { const names = useBearFamilyMealsStore( useShallow((state) => Object.keys(state)), ) return
{names.join(', ')}
} export default function App() { return ( <> ) } ``` By using `useShallow`, we optimized the rendering process, ensuring that the component only re-renders when necessary, which improves overall performance. ## Troubleshooting TBD ]]>
(stateCreatorFn: StateCreator, persistOptions?: PersistOptions): StateCreator ``` ### Mutator ```ts ;['zustand/persist', U] ``` ## Reference ### `persist(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. - `persistOptions`: An object to define storage options. - `name`: A unique name of the item for your store in the storage. - **optional** `storage`: Defaults to `createJSONStorage(() => localStorage)`. - **optional** `partialize`: A function to filter state fields before persisting it. - **optional** `onRehydrateStorage`: A function or function returning a function that allows custom logic before and after state rehydration. - **optional** `version`: A version number for the persisted state. If the stored state version doesn't match, it won't be used. - **optional** `migrate`: A function to migrate persisted state if the version mismatch occurs. - **optional** `merge`: A function for custom logic when merging persisted state with the current state during rehydration. Defaults to a shallow merge. - **optional** `skipHydration`: Defaults to `false`. If `true`, the middleware won't automatically rehydrate the state on initialization. Use `rehydrate` function manually in this case. This is useful for server-side rendering (SSR) applications. #### Returns `persist` returns a state creator function. ## Usage ### Persisting a state In this tutorial, we'll create a simple position tracker using vanilla store and the `persist` middleware. The example tracks the `position` of the mouse as it moves within a container and stores the `position` in local storage, so it persists even when the page reloads. We start by setting up a vanilla store that holds the position (an object with `x` and `y` coordinates) and an action to update it. We'll also use the `persist` middleware to store the position in `localStorage`. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage' }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage' }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state partially In this tutorial, we'll create a simple position tracker using vanilla store and the `persist` middleware. Additionally, we'll show you how to persist only part of the state (partial persistence), which can be useful when you don’t want to store the entire state in `localStorage`. We’ll first create a vanilla store that holds the position state and actions to update it. We'll use the `persist` middleware to persist only the relevant part of the state (in this case, the context containing the position). ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { context: { position: { x: number; y: number } } } type PositionStoreActions = { actions: { setPosition: ( nextPosition: PositionStoreState['context']['position'], ) => void } } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ context: { position: { x: 0, y: 0 }, }, actions: { setPosition: (position) => set({ context: { position } }), }, }), { name: 'position-storage', partialize: (state) => ({ context: state.context }), }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().actions.setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.context.position.x}px, ${state.context.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the full code to create a dot that follows your mouse movement inside a container and persists the `context` in `localStorage`. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { context: { position: { x: number; y: number } } } type PositionStoreActions = { actions: { setPosition: ( nextPosition: PositionStoreState['context']['position'], ) => void } } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ context: { position: { x: 0, y: 0 }, }, actions: { setPosition: (position) => set({ context: { position } }), }, }), { name: 'position-storage', partialize: (state) => ({ context: state.context }), }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().actions.setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.context.position.x}px, ${state.context.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state with custom storage In this mini tutorial, we’ll create a simple position-tracking system using vanilla store, where the position state is persisted in the URL's search parameters. This approach allows state persistence directly in the browser's URL, which can be useful for maintaining state across page reloads or sharing links with state embedded. We need to implement functions to manipulate URL search parameters as if they were a storage mechanism. This includes retrieving, setting, and removing parameters. ```ts const getSearchParams = () => { return new URL(location.href).searchParams } const updateSearchParams = (searchParams: URLSearchParams) => { window.history.replaceState( {}, '', `${location.pathname}?${searchParams.toString()}`, ) } const getSearchParam = (key: string) => { const searchParams = getSearchParams() return searchParams.get(key) } const updateSearchParam = (key: string, value: string) => { const searchParams = getSearchParams() searchParams.set(key, value) updateSearchParams(searchParams) } const removeSearchParam = (key: string) => { const searchParams = getSearchParams() searchParams.delete(key) updateSearchParams(searchParams) } ``` To use the URL search parameters as storage, we define a `searchParamsStorage` object with `getItem`, `setItem`, and `removeItem` methods. These methods map to our custom functions that manipulate search parameters. ```ts const searchParamsStorage = { getItem: (key: string) => getSearchParam(key), setItem: (key: string, value: string) => updateSearchParam(key, value), removeItem: (key: string) => removeSearchParam(key), } ``` Now, we initialize the vanilla store using the `persist` middleware, specifying that we want to use our custom storage. Instead of the default `localStorage` or `sessionStorage`, we’ll persist the position data in the URL search parameters. ```ts import { createStore } from 'zustand/vanilla' import { persist, createJSONStorage } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', storage: createJSONStorage(() => searchParamsStorage), }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the full code to create a dot that follows your mouse movement inside a container and persists the position in URL's search parameters. ```ts import { createStore } from 'zustand/vanilla' import { persist, createJSONStorage } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const getSearchParams = () => { return new URL(location.href).searchParams } const updateSearchParams = (searchParams: URLSearchParams) => { window.history.replaceState( {}, '', `${location.pathname}?${searchParams.toString()}`, ) } const getSearchParam = (key: string) => { const searchParams = getSearchParams() return searchParams.get(key) } const updateSearchParam = (key: string, value: string) => { const searchParams = getSearchParams() searchParams.set(key, value) updateSearchParams(searchParams) } const removeSearchParam = (key: string) => { const searchParams = getSearchParams() searchParams.delete(key) updateSearchParams(searchParams) } const searchParamsStorage = { getItem: (key: string) => getSearchParam(key), setItem: (key: string, value: string) => updateSearchParam(key, value), removeItem: (key: string) => removeSearchParam(key), } const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', storage: createJSONStorage(() => searchParamsStorage), }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state through versioning and migrations In this tutorial, we’ll explore how to manage state persistence using versioning and migration. We will demonstrate how to evolve your state schema across versions without breaking existing persisted data. Before moving to versioned state management, we simulate an initial state for `version` 0. This is done by manually setting a `version` 0 state in `localStorage` if it doesn't already exist. The `version` 0 state saves the coordinates as `x` and `y` fields. ```ts // For tutorial purposes only if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { x: 100, y: 100 }, // version 0 structure version: 0, }), ) } ``` Next, we use `persist` middleware to handle state persistence. We also add a migration function to handle changes between versions. In this example, we `migrate` the state from `version` 0 (where `x` and `y` are separate) to `version` 1, where they are combined into a `position` object. ```ts migrate: (persisted: any, version) => { if (version === 0) { persisted.position = { x: persisted.x, y: persisted.y } delete persisted.x delete persisted.y } return persisted } ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' // For tutorial purposes only if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { x: 100, y: 100 }, version: 0, }), ) } type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, // version 0: just x: 0, y: 0 setPosition: (position) => set({ position }), }), { name: 'position-storage', version: 1, migrate: (persisted: any, version) => { if (version === 0) { persisted.position = { x: persisted.x, y: persisted.y } delete persisted.x delete persisted.y } return persisted }, }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state with nested objects In this tutorial, we’ll create a vanilla store that keeps track of a position represented by `x` and `y` coordinates. We will also implement persistence using `localStorage` and demonstrate how to handle merging of state with potentially missing fields. To simulate an initial state for the tutorial, we will check if our position data exists in `localStorage`. If it doesn't, we’ll set it up. ```ts if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { position: { y: 100 } }, // missing `x` field version: 0, }), ) } ``` Now, we will create the store and configure it to use persistence and deep merging. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' import createDeepMerge from '@fastify/deepmerge' const deepMerge = createDeepMerge({ all: true }) type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', merge: (persisted, current) => deepMerge(current, persisted) as never, }, ), ) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' import createDeepMerge from '@fastify/deepmerge' const deepMerge = createDeepMerge({ all: true }) // For tutorial purposes only if (!localStorage.getItem('position-storage')) { localStorage.setItem( 'position-storage', JSON.stringify({ state: { position: { y: 100 } }, // missing `x` field version: 0, }), ) } type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', merge: (persisted, current) => deepMerge(current, persisted) as never, }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { console.log({ state }) $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ### Persisting a state and hydrate it manually In this tutorial, we’ll create a vanilla store that keeps track of a position represented by `x` and `y` coordinates. We will also implement persistence using `localStorage` and explore how to skip the hydration process and manually trigger rehydration after a delay. We start by setting up a vanilla store that holds the position (an object with `x` and `y` coordinates) and an action to update it. Furthermore, we'll also use the `persist` middleware to store the position in `localStorage` but skipping hydration. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', skipHydration: true, }, ), ) ``` Since we skipped hydration in the initial setup, we will manually rehydrate the state. Here, we’re using `setTimeout` to simulate a delayed rehydration. ```ts setTimeout(() => { positionStore.persist.rehydrate() }, 2000) ``` Next, we'll track the mouse movements inside a div and update the store with the new position. ```ts const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) ``` We want to reflect the position updates on the screen by moving a div element (representing the dot) to the new coordinates. ```ts const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here’s the complete code. ```ts import { createStore } from 'zustand/vanilla' import { persist } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( persist( (set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), }), { name: 'position-storage', skipHydration: true, }, ), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } setTimeout(() => { positionStore.persist.rehydrate() }, 2000) render(positionStore.getState(), positionStore.getState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ## Troubleshooting TBD ]]>
[!IMPORTANT] > In order to use `devtools` from `zustand/middleware` you need to install > `@redux-devtools/extension` library. ```js const nextStateCreatorFn = devtools(stateCreatorFn, devtoolsOptions) ``` - [Types](#types) - [Signature](#signature) - [Mutator](#mutator) - [Reference](#reference) - [Usage](#usage) - [Debugging a store](#debugging-a-store) - [Debugging a Slices pattern based store](#debugging-a-slices-pattern-based-store) - [Filtering actions with actionsDenylist](#filtering-actions-with-actionsdenylist) - [Cleanup](#cleanup) - [Troubleshooting](#troubleshooting) - [Only one store is displayed](#only-one-store-is-displayed) - [Action names are labeled as 'anonymous'](#all-action-names-are-labeled-as-anonymous) ## Types ### Signature ```ts devtools(stateCreatorFn: StateCreator, devtoolsOptions?: DevtoolsOptions): StateCreator ``` ### Mutator ```ts ;['zustand/devtools', never] ``` ## Reference ### `devtools(stateCreatorFn, devtoolsOptions)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. - **optional** `devtoolsOptions`: An object to define `Redux Devtools` options. - **optional** `name`: A custom identifier for the connection in the Redux DevTools. - **optional** `enabled`: Defaults to `true` when is on development mode, and defaults to `false` when is on production mode. Enables or disables the Redux DevTools integration for this store. - **optional** `anonymousActionType`: Defaults to the inferred action type or `anonymous` if unavailable. A string to use as the action type for anonymous mutations in the Redux DevTools. - **optional** `store`: A custom identifier for the store in the Redux DevTools. - **optional** `actionsDenylist`: A string or array of strings (regex patterns) that specify which actions should be filtered out from Redux DevTools. This option is passed directly to Redux DevTools for filtering. For example, `['secret.*']` will filter out all actions starting with "secret". #### Returns `devtools` returns a state creator function. ## Usage ### Debugging a store This example shows you how you can use `Redux Devtools` to debug a store ```ts import { create, StateCreator } from 'zustand' import { devtools } from 'zustand/middleware' type JungleStore = { bears: number addBear: () => void fishes: number addFish: () => void } const useJungleStore = create()( devtools((set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 }), undefined, 'jungle/addBear'), fishes: 0, addFish: () => set( (state) => ({ fishes: state.fishes + 1 }), undefined, 'jungle/addFish', ), })), ) ``` ### Debugging a Slices pattern based store This example shows you how you can use `Redux Devtools` to debug a Slices pattern based store ```ts import { create, StateCreator } from 'zustand' import { devtools } from 'zustand/middleware' type BearSlice = { bears: number addBear: () => void } type FishSlice = { fishes: number addFish: () => void } type JungleStore = BearSlice & FishSlice const createBearSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set( (state) => ({ bears: state.bears + 1 }), undefined, 'jungle:bear/addBear', ), }) const createFishSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set( (state) => ({ fishes: state.fishes + 1 }), undefined, 'jungle:fish/addFish', ), }) const useJungleStore = create()( devtools((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), })), ) ``` ### Filtering actions with actionsDenylist You can filter out specific actions from Redux DevTools using the `actionsDenylist` option. This is useful for hiding internal or sensitive actions from the DevTools timeline. ```ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' type Store = { user: string | null token: string | null login: (user: string, token: string) => void logout: () => void updateData: () => void } const useStore = create()( devtools( (set) => ({ user: null, token: null, login: (user, token) => set({ user, token }, undefined, 'auth/login'), logout: () => set({ user: null, token: null }, undefined, 'auth/logout'), updateData: () => set({ user: 'updated' }, undefined, 'internal/updateData'), }), { name: 'AuthStore', // Filter out actions matching these regex patterns actionsDenylist: ['internal/.*'], // Hides all 'internal/*' actions }, ), ) ``` You can also use a single regex string: ```ts const useStore = create()( devtools( (set) => ({ // ... state and actions }), { name: 'MyStore', actionsDenylist: 'secret.*', // Hides all actions starting with 'secret' }, ), ) ``` > [!NOTE] > The `actionsDenylist` option uses regex pattern matching and is handled directly by Redux DevTools Extension. > All actions are still sent to DevTools, but matching actions are filtered from the display. ### Cleanup When a store is no longer needed, you can clean up the Redux DevTools connection by calling the `cleanup` method on the store: ```ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' const useStore = create( devtools((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })), ) // When you're done with the store, clean it up useStore.devtools.cleanup() ``` This is particularly useful in applications that wrap store in context or create multiple stores dynamically. ## Troubleshooting ### Only one store is displayed By default, `Redux Devtools` only show one store at a time, so in order to see other stores you need to use store selector and choose a different store. ### All action names are labeled as 'anonymous' If an action type name is not provided, it is defaulted to "anonymous". You can customize this default value by providing a `anonymousActionType` parameter: For instance the next example doesn't have action type name: ```ts import { create, StateCreator } from 'zustand' import { devtools } from 'zustand/middleware' type BearSlice = { bears: number addBear: () => void } type FishSlice = { fishes: number addFish: () => void } type JungleStore = BearSlice & FishSlice const createBearSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), }) const createFishSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) const useJungleStore = create()( devtools((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), })), ) ``` In order to fix the previous example, we need to provide an action type name as the third parameter. Additionally, to preserve the default behavior of the replacement logic, the second parameter should be set to `undefined`. Here's the fixed previous example ```ts import { create, StateCreator } from 'zustand' type BearSlice = { bears: number addBear: () => void } type FishSlice = { fishes: number addFish: () => void } type JungleStore = BearSlice & FishSlice const createBearSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], BearSlice > = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 }), undefined, 'bear/addBear'), }) const createFishSlice: StateCreator< JungleStore, [['zustand/devtools', never]], [], FishSlice > = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 }), undefined, 'fish/addFish'), }) const useJungleStore = create()( devtools((...args) => ({ ...createBearSlice(...args), ...createFishSlice(...args), })), ) ``` > [!IMPORTANT] > Do not set the second parameter to `true` or `false` unless you want to override the default > replacement logic ]]> (reducerFn: (state: T, action: A) => T, initialState: T): StateCreator A }, [['zustand/redux', A]], []> ``` ### Mutator ```ts ;['zustand/redux', A] ``` ## Reference ### `redux(reducerFn, initialState)` #### Parameters - `reducerFn`: It should be pure and should take the current state of your application and an action object as arguments, and returns the new state resulting from applying the action. - `initialState`: The value you want the state to be initially. It can be a value of any type, except a function. #### Returns `redux` returns a state creator function. ## Usage ### Updating state through actions and reducers ```ts import { createStore } from 'zustand/vanilla' import { redux } from 'zustand/middleware' type PersonStoreState = { firstName: string lastName: string email: string } type PersonStoreAction = | { type: 'person/setFirstName'; firstName: string } | { type: 'person/setLastName'; lastName: string } | { type: 'person/setEmail'; email: string } type PersonStore = PersonStoreState & { dispatch: (action: PersonStoreAction) => PersonStoreAction } const personStoreReducer = ( state: PersonStoreState, action: PersonStoreAction, ) => { switch (action.type) { case 'person/setFirstName': { return { ...state, firstName: action.firstName } } case 'person/setLastName': { return { ...state, lastName: action.lastName } } case 'person/setEmail': { return { ...state, email: action.email } } default: { return state } } } const personStoreInitialState: PersonStoreState = { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', } const personStore = createStore()( redux(personStoreReducer, personStoreInitialState), ) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.dispatch({ type: 'person/setFirstName', firstName: (event.target as any).value, }) } function handleLastNameChange(event: Event) { personStore.dispatch({ type: 'person/setLastName', lastName: (event.target as any).value, }) } function handleEmailChange(event: Event) { personStore.dispatch({ type: 'person/setEmail', email: (event.target as any).value, }) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (person) => { $firstNameInput.value = person.firstName $lastNameInput.value = person.lastName $emailInput.value = person.email $result.innerHTML = `${person.firstName} ${person.lastName} (${person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` Here's the `html` code ```html

``` ## Troubleshooting TBD ]]>
[!IMPORTANT] > In order to use `immer` from `zustand/middleware/immer` you need to install > `immer` library. ```js const nextStateCreatorFn = immer(stateCreatorFn) ``` - [Types](#types) - [Signature](#signature) - [Mutator](#mutator) - [Reference](#reference) - [Usage](#usage) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts immer(stateCreatorFn: StateCreator): StateCreator ``` ### Mutator ```ts ;['zustand/immer', never] ``` ## Reference ### `immer(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `immer` returns a state creator function. ## Usage ### Updating state without boilerplate code In the next example, we're going to update the `person` object. Since it's a nested object, we need to create a copy of the entire object before making the update. ```ts import { createStore } from 'zustand/vanilla' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: ( nextPerson: ( person: PersonStoreState['person'], ) => PersonStoreState['person'] | PersonStoreState['person'], ) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set((state) => ({ person: typeof nextPerson === 'function' ? nextPerson(state.person) : nextPerson, })), })) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().setPerson((person) => ({ ...person, firstName: (event.target as any).value, })) } function handleLastNameChange(event: Event) { personStore.getState().setPerson((person) => ({ ...person, lastName: (event.target as any).value, })) } function handleEmailChange(event: Event) { personStore.getState().setPerson((person) => ({ ...person, email: (event.target as any).value, })) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` Here's the `html` code ```html

``` To avoid manually copying the entire object before making updates, we'll use the `immer` middleware. ```ts import { createStore } from 'zustand/vanilla' import { immer } from 'zustand/middleware/immer' type PersonStoreState = { person: { firstName: string; lastName: string; email: string } } type PersonStoreActions = { setPerson: ( nextPerson: ( person: PersonStoreState['person'], ) => PersonStoreState['person'] | PersonStoreState['person'], ) => void } type PersonStore = PersonStoreState & PersonStoreActions const personStore = createStore()( immer((set) => ({ person: { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }, setPerson: (nextPerson) => set((state) => { state.person = typeof nextPerson === 'function' ? nextPerson(state.person) : nextPerson }), })), ) const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.getState().setPerson((person) => { person.firstName = (event.target as any).value }) } function handleLastNameChange(event: Event) { personStore.getState().setPerson((person) => { person.lastName = (event.target as any).value }) } function handleEmailChange(event: Event) { personStore.getState().setPerson((person) => { person.email = (event.target as any).value }) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters[0] = (state) => { $firstNameInput.value = state.person.firstName $lastNameInput.value = state.person.lastName $emailInput.value = state.person.email $result.innerHTML = `${state.person.firstName} ${state.person.lastName} (${state.person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render) ``` ## Troubleshooting TBD ]]>
[!TIP] > This makes state management more straightforward and efficient by making curried version of > `create` and `createStore` not necessary for middleware usage. ```js const nextStateCreatorFn = combine(initialState, additionalStateCreatorFn) ``` - [Types](#types) - [Signature](#combine-signature) - [Reference](#reference) - [Usage](#usage) - [Creating a state with inferred types](#creating-a-state-wit-inferred-types) - [Troubleshooting](#troubleshooting) ## Types ### Signature ```ts combine(initialState: T, additionalStateCreatorFn: StateCreator): StateCreator & U, [], []> ``` ## Reference ### `combine(initialState, additionalStateCreatorFn)` #### Parameters - `initialState`: The value you want the state to be initially. It can be a value of any type, except a function. - `additionalStateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `combine` returns a state creator function. ## Usage ### Creating a store with inferred types This example shows you how you can create a store and get types automatically inferred, so you don’t need to define them explicitly. ```ts import { createStore } from 'zustand/vanilla' import { combine } from 'zustand/middleware' const positionStore = createStore( combine({ position: { x: 0, y: 0 } }, (set) => ({ setPosition: (position) => set({ position }), })), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) ``` Here's the `html` code ```html
``` ## Troubleshooting TBD ]]>
(stateCreatorFn: StateCreator): StateCreator ``` ### Mutator ```ts ;['zustand/subscribeWithSelector', never] ``` ## Reference ### `subscribeWithSelector(stateCreatorFn)` #### Parameters - `stateCreatorFn`: A function that takes `set` function, `get` function and `store` as arguments. Usually, you will return an object with the methods you want to expose. #### Returns `subscribeWithSelector` returns a state creator function. ## Usage ### Subscribing partial state updates By subscribing to partial state updates, you register a callback that fires whenever the store's partial state updates. We can use `subscribe` for external state management. ```ts import { createStore } from 'zustand/vanilla' import { subscribeWithSelector } from 'zustand/middleware' type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore()( subscribeWithSelector((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })), ) const $dot = document.getElementById('dot') as HTMLDivElement $dot.addEventListener('mouseenter', (event) => { const parent = event.currentTarget.parentElement const parentWidth = parent.clientWidth const parentHeight = parent.clientHeight positionStore.getState().setPosition({ x: Math.ceil(Math.random() * parentWidth), y: Math.ceil(Math.random() * parentHeight), }) }) const render: Parameters[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe((state) => state.position, render) const logger: Parameters[0] = (x) => { console.log('new x position', { x }) } positionStore.subscribe((state) => state.position.x, logger) ``` Here's the `html` code ```html
``` ## Troubleshooting TBD ]]>
({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // name of the item in the storage (must be unique) storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used }, ), ) ``` ## Typescript simple example ```ts import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' type BearStore = { bears: number addABear: () => void } export const useBearStore = create()( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // name of the item in the storage (must be unique) storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used }, ), ) ``` ## Options ### `name` This is the only required option. The given name is going to be the key used to store your Zustand state in the storage, so it must be unique. ### `storage` > Type: `() => StateStorage` The `StateStorage` can be imported with: ```ts import { StateStorage } from 'zustand/middleware' ``` > Default: `createJSONStorage(() => localStorage)` Enables you to use your own storage. Simply pass a function that returns the storage you want to use. It's recommended to use the [`createJSONStorage`](#createjsonstorage) helper function to create a `storage` object that is compliant with the `StateStorage` interface. Example: ```ts import { persist, createJSONStorage } from 'zustand/middleware' export const useBoundStore = create( persist( (set, get) => ({ // ... }), { // ... storage: createJSONStorage(() => AsyncStorage), }, ), ) ``` ### `partialize` > Type: `(state: Object) => Object` > Default: `(state) => state` Enables you to pick some of the state's fields to be stored in the storage. You could omit multiple fields using the following: ```ts export const useBoundStore = create( persist( (set, get) => ({ foo: 0, bar: 1, }), { // ... partialize: (state) => Object.fromEntries( Object.entries(state).filter(([key]) => !['foo'].includes(key)), ), }, ), ) ``` Or you could allow only specific fields using the following: ```ts export const useBoundStore = create( persist( (set, get) => ({ foo: 0, bar: 1, }), { // ... partialize: (state) => ({ foo: state.foo }), }, ), ) ``` ### `onRehydrateStorage` > Type: `(state: Object) => ((state?: Object, error?: Error) => void) | void` This option enables you to pass a listener function that will be called when the storage is hydrated. Example: ```ts export const useBoundStore = create( persist( (set, get) => ({ // ... }), { // ... onRehydrateStorage: (state) => { console.log('hydration starts') // optional return (state, error) => { if (error) { console.log('an error happened during hydration', error) } else { console.log('hydration finished') } } }, }, ), ) ``` ### `version` > Type: `number` > Default: `0` If you want to introduce a breaking change in your storage (e.g. renaming a field), you can specify a new version number. By default, if the version in the storage does not match the version in the code, the stored value won't be used. You can use the [migrate](#migrate) function (see below) to handle breaking changes in order to persist previously stored data. ### `migrate` > Type: `(persistedState: Object, version: number) => Object | Promise` > Default: `(persistedState) => persistedState` You can use this option to handle versions migration. The migrate function takes the persisted state and the version number as arguments. It must return a state that is compliant to the latest version (the version in the code). For instance, if you want to rename a field, you can use the following: ```ts export const useBoundStore = create( persist( (set, get) => ({ newField: 0, // let's say this field was named otherwise in version 0 }), { // ... version: 1, // a migration will be triggered if the version in the storage mismatches this one migrate: (persistedState, version) => { if (version === 0) { // if the stored value is in version 0, we rename the field to the new name persistedState.newField = persistedState.oldField delete persistedState.oldField } return persistedState }, }, ), ) ``` ### `merge` > Type: `(persistedState: Object, currentState: Object) => Object` > Default: `(persistedState, currentState) => ({ ...currentState, ...persistedState })` In some cases, you might want to use a custom merge function to merge the persisted value with the current state. By default, the middleware does a shallow merge. The shallow merge might not be enough if you have partially persisted nested objects. For instance, if the storage contains the following: ```ts { foo: { bar: 0, } } ``` But your Zustand store contains: ```ts { foo: { bar: 0, baz: 1, } } ``` The shallow merge will erase the `baz` field from the `foo` object. One way to fix this would be to give a custom deep merge function: ```ts export const useBoundStore = create( persist( (set, get) => ({ foo: { bar: 0, baz: 1, }, }), { // ... merge: (persistedState, currentState) => deepMerge(currentState, persistedState), }, ), ) ``` ### `skipHydration` > Type: `boolean | undefined` > Default: `undefined` By default the store will be hydrated on initialization. In some applications you may need to control when the first hydration occurs. For example, in server-rendered apps. If you set `skipHydration`, the initial call for hydration isn't called, and it is left up to you to manually call `rehydrate()`. ```ts export const useBoundStore = create( persist( () => ({ count: 0, // ... }), { // ... skipHydration: true, }, ), ) ``` ```tsx import { useBoundStore } from './path-to-store'; export function StoreConsumer() { // hydrate persisted store after on mount useEffect(() => { useBoundStore.persist.rehydrate(); }, []) return ( //... ) } ``` ## API > Version: >=3.6.3 The Persist API enables you to do a number of interactions with the Persist middleware from inside or outside of a React component. ### `getOptions` > Type: `() => Partial` > Returns: Options of the Persist middleware For example, it can be used to obtain the storage name: ```ts useBoundStore.persist.getOptions().name ``` ### `setOptions` > Type: `(newOptions: Partial) => void` Changes the middleware options. Note that the new options will be merged with the current ones. For instance, this can be used to change the storage name: ```ts useBoundStore.persist.setOptions({ name: 'new-name', }) ``` Or even to change the storage engine: ```ts useBoundStore.persist.setOptions({ storage: createJSONStorage(() => sessionStorage), }) ``` ### `clearStorage` > Type: `() => void` Clears everything stored under the [name](#name) key. ```ts useBoundStore.persist.clearStorage() ``` ### `rehydrate` > Type: `() => Promise` In some cases, you might want to trigger the rehydration manually. This can be done by calling the `rehydrate` method. ```ts await useBoundStore.persist.rehydrate() ``` ### `hasHydrated` > Type: `() => boolean` This is a non-reactive getter to check if the storage has been hydrated (note that it updates when calling [`rehydrate`](#rehydrate)). ```ts useBoundStore.persist.hasHydrated() ``` ### `onHydrate` > Type: `(listener: (state) => void) => () => void` > Returns: Unsubscribe function This listener will be called when the hydration process starts. ```ts const unsub = useBoundStore.persist.onHydrate((state) => { console.log('hydration starts') }) // later on... unsub() ``` ### `onFinishHydration` > Type: `(listener: (state) => void) => () => void` > Returns: Unsubscribe function This listener will be called when the hydration process ends. ```ts const unsub = useBoundStore.persist.onFinishHydration((state) => { console.log('hydration finished') }) // later on... unsub() ``` ### `createJSONStorage` > Type: `(getStorage: () => StateStorage, options?: JsonStorageOptions) => StateStorage` > Returns: `PersistStorage` This helper function enables you to create a [`storage`](#storage) object which is useful when you want to use a custom storage engine. `getStorage` is a function that returns the storage engine with the properties `getItem`, `setItem`, and `removeItem`. `options` is an optional object that can be used to customize the serialization and deserialization of the data. `options.reviver` is a function that is passed to `JSON.parse` to deserialize the data. `options.replacer` is a function that is passed to `JSON.stringify` to serialize the data. ```ts import { createJSONStorage } from 'zustand/middleware' const storage = createJSONStorage(() => sessionStorage, { reviver: (key, value) => { if (value && value.type === 'date') { return new Date(value) } return value }, replacer: (key, value) => { // NOTE: the result of `.toJSON()` is passed to the // replacer function as value if is available so // a Date is always a `string` at this point if (key === 'someDate') return { type: 'date', value } return value }, }) ``` ## Hydration and asynchronous storages To explain what is the "cost" of asynchronous storages, you need to understand what is hydration. In a nutshell, hydration is a process of retrieving persisted state from the storage and merging it with the current state. The Persist middleware does two kinds of hydration: synchronous and asynchronous. If the given storage is synchronous (e.g., `localStorage`), hydration will be done synchronously. On the other hand, if the given storage is asynchronous (e.g., `AsyncStorage`), hydration will be done asynchronously (shocking, I know!). But what's the catch? With synchronous hydration, the Zustand store will already have been hydrated at its creation. In contrast, with asynchronous hydration, the Zustand store will be hydrated later on, in a microtask. Why does it matter? Asynchronous hydration can cause some unexpected behaviors. For instance, if you use Zustand in a React app, the store will **not** be hydrated at the initial render. In cases where your app depends on the persisted value at page load, you might want to wait until the store has been hydrated before showing anything. For example, your app might think the user is not logged in because it's the default, but in reality the store has not been hydrated yet. If your app does depends on the persisted state at page load, see [_How can I check if my store has been hydrated_](#how-can-i-check-if-my-store-has-been-hydrated) in the [FAQ](#faq) section below. ### Usage in Next.js NextJS uses Server Side Rendering, and it will compare the rendered component on the server with the one rendered on client. But since you are using data from browser to change your component, the two renders will differ and Next will throw a warning at you. The errors usually are: - Text content does not match server-rendered HTML - Hydration failed because the initial UI does not match what was rendered on the server - There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering To solve these errors, create a custom hook so that Zustand waits a little before changing your components. Create a file with the following: ```ts // useStore.ts import { useState, useEffect } from 'react' const useStore = ( store: (callback: (state: T) => unknown) => unknown, callback: (state: T) => F, ) => { const result = store(callback) as F const [data, setData] = useState() useEffect(() => { setData(result) }, [result]) return data } export default useStore ``` Now in your pages, you will use the hook a little bit differently: ```ts // useBearStore.ts import { create } from 'zustand' import { persist } from 'zustand/middleware' // the store itself does not need any change export const useBearStore = create( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', }, ), ) ``` ```ts // yourComponent.tsx import useStore from './useStore' import { useBearStore } from './stores/useBearStore' const bears = useStore(useBearStore, (state) => state.bears) ``` Credits: [This reply to an issue](https://github.com/pmndrs/zustand/issues/938#issuecomment-1481801942), which points to [this blog post](https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5). ## FAQ ### How can I check if my store has been hydrated There are a few different ways to do this. You can use the [`onRehydrateStorage`](#onrehydratestorage) listener function to update a field in the store: ```ts const useBoundStore = create( persist( (set, get) => ({ // ... _hasHydrated: false, setHasHydrated: (state) => { set({ _hasHydrated: state }); } }), { // ... onRehydrateStorage: (state) => { return () => state.setHasHydrated(true) } } ) ); export default function App() { const hasHydrated = useBoundStore(state => state._hasHydrated); if (!hasHydrated) { return

Loading...

} return ( // ... ); } ``` You can also create a custom `useHydration` hook: ```ts const useBoundStore = create(persist(...)) const useHydration = () => { const [hydrated, setHydrated] = useState(false) useEffect(() => { // Note: This is just in case you want to take into account manual rehydration. // You can remove the following line if you don't need it. const unsubHydrate = useBoundStore.persist.onHydrate(() => setHydrated(false)) const unsubFinishHydration = useBoundStore.persist.onFinishHydration(() => setHydrated(true)) setHydrated(useBoundStore.persist.hasHydrated()) return () => { unsubHydrate() unsubFinishHydration() } }, []) return hydrated } ``` ### How can I use a custom storage engine If the storage you want to use does not match the expected API, you can create your own storage: ```ts import { create } from 'zustand' import { persist, createJSONStorage, StateStorage } from 'zustand/middleware' import { get, set, del } from 'idb-keyval' // can use anything: IndexedDB, Ionic Storage, etc. // Custom storage object const storage: StateStorage = { getItem: async (name: string): Promise => { console.log(name, 'has been retrieved') return (await get(name)) || null }, setItem: async (name: string, value: string): Promise => { console.log(name, 'with value', value, 'has been saved') await set(name, value) }, removeItem: async (name: string): Promise => { console.log(name, 'has been deleted') await del(name) }, } export const useBoundStore = create( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // unique name storage: createJSONStorage(() => storage), }, ), ) ``` If you're using a type that `JSON.stringify()` doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data. For example, [Superjson](https://github.com/blitz-js/superjson) can serialize data along with its type, allowing the data to be parsed back to its original type upon deserialization ```ts import superjson from 'superjson' // can use anything: serialize-javascript, devalue, etc. import { PersistStorage } from 'zustand/middleware' interface BearState { bear: Map fish: Set time: Date query: RegExp } const storage: PersistStorage = { getItem: (name) => { const str = localStorage.getItem(name) if (!str) return null return superjson.parse(str) }, setItem: (name, value) => { localStorage.setItem(name, superjson.stringify(value)) }, removeItem: (name) => localStorage.removeItem(name), } const initialState: BearState = { bear: new Map(), fish: new Set(), time: new Date(), query: new RegExp(''), } export const useBearStore = create()( persist( (set) => ({ ...initialState, // ... }), { name: 'food-storage', storage, }, ), ) ``` ### How can I rehydrate on storage event You can use the Persist API to create your own implementation, similar to the example below: ```ts type StoreWithPersist = Mutate, [["zustand/persist", unknown]] > export const withStorageDOMEvents = (store: StoreWithPersist) => { const storageEventCallback = (e: StorageEvent) => { if (e.key === store.persist.getOptions().name && e.newValue) { store.persist.rehydrate() } } window.addEventListener('storage', storageEventCallback) return () => { window.removeEventListener('storage', storageEventCallback) } } const useBoundStore = create(persist(...)) withStorageDOMEvents(useBoundStore) ``` ### How do I use it with TypeScript Basic typescript usage doesn't require anything special except for writing `create()(...)` instead of `create(...)`. ```tsx import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' interface MyState { bears: number addABear: () => void } export const useBearStore = create()( persist( (set, get) => ({ bears: 0, addABear: () => set({ bears: get().bears + 1 }), }), { name: 'food-storage', // name of item in the storage (must be unique) storage: createJSONStorage(() => sessionStorage), // (optional) by default the 'localStorage' is used partialize: (state) => ({ bears: state.bears }), }, ), ) ``` ### How do I use it with Map and Set In order to persist object types such as `Map` and `Set`, they will need to be converted to JSON-serializable types such as an `Array` which can be done by defining a custom `storage` engine. Let's say your state uses `Map` to handle a list of `transactions`, then you can convert the `Map` into an `Array` in the `storage` prop which is shown below: ```ts interface BearState { . . . transactions: Map } storage: { getItem: (name) => { const str = localStorage.getItem(name); if (!str) return null; const existingValue = JSON.parse(str); return { ...existingValue, state: { ...existingValue.state, transactions: new Map(existingValue.state.transactions), } } }, setItem: (name, newValue: StorageValue) => { // functions cannot be JSON encoded const str = JSON.stringify({ ...newValue, state: { ...newValue.state, transactions: Array.from(newValue.state.transactions.entries()), }, }) localStorage.setItem(name, str) }, removeItem: (name) => localStorage.removeItem(name), }, ``` ]]> void decrement: (qty: number) => void } export const useCountStore = create()( immer((set) => ({ count: 0, increment: (qty: number) => set((state) => { state.count += qty }), decrement: (qty: number) => set((state) => { state.count -= qty }), })), ) ``` Updating complex states ```ts import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface Todo { id: string title: string done: boolean } type State = { todos: Record } type Actions = { toggleTodo: (todoId: string) => void } export const useTodoStore = create()( immer((set) => ({ todos: { '82471c5f-4207-4b1d-abcb-b98547e01a3e': { id: '82471c5f-4207-4b1d-abcb-b98547e01a3e', title: 'Learn Zustand', done: false, }, '354ee16c-bfdd-44d3-afa9-e93679bda367': { id: '354ee16c-bfdd-44d3-afa9-e93679bda367', title: 'Learn Jotai', done: false, }, '771c85c5-46ea-4a11-8fed-36cc2c7be344': { id: '771c85c5-46ea-4a11-8fed-36cc2c7be344', title: 'Learn Valtio', done: false, }, '363a4bac-083f-47f7-a0a2-aeeee153a99c': { id: '363a4bac-083f-47f7-a0a2-aeeee153a99c', title: 'Learn Signals', done: false, }, }, toggleTodo: (todoId: string) => set((state) => { state.todos[todoId].done = !state.todos[todoId].done }), })), ) ``` ## Gotchas In this section you will find some things that you need to keep in mind when using Zustand with Immer. ### My subscriptions aren't being called If you are using Immer, make sure you are actually following [the rules of Immer](https://immerjs.github.io/immer/pitfalls). For example, you have to add `[immerable] = true` for [class objects](https://immerjs.github.io/immer/complex-objects) to work. If you don't do this, Immer will still mutate the object, but not as a proxy, so it will also update the current state. Zustand checks if the state has actually changed, so since both the current state and the next state are equal (if you don't do it correctly), Zustand will skip calling the subscriptions. ## Demos - Basic: https://stackblitz.com/edit/vitejs-vite-3sgc4ejy - Advanced: https://stackblitz.com/edit/vitejs-vite-jxxtuyj3 ]]> Disclaimer: These libraries may have bugs, limited maintenance, > or other limitations, and are not officially recommended > by pmndrs or the Zustand maintainers. > This list aims to provide a good starting point > for someone looking to extend Zustand's feature set. - [@colorfy-software/zfy](https://colorfy-software.gitbook.io/zfy/) — 🧸 Useful helpers for state management in React with Zustand. - [@csark0812/zustand-expo-devtools](https://github.com/csark0812/zustand-expo-devtools) — 🧭 Connect Zustand to Redux DevTools in Expo + React Native using the official Expo DevTools plugin system. - [@csark0812/zustand-getters](https://github.com/csark0812/zustand-getters) — 🔄 Make JavaScript object getters reactive in Zustand stores — define derived values with `get propertyName()` and they automatically trigger subscription updates when accessed. - [@davstack/store](https://www.npmjs.com/package/@davstack/store) — A zustand store factory that auto generates selectors with get/set/use methods, supports inferred types, and makes global / local state management easy. - [@dhmk/zustand-lens](https://github.com/dhmk083/dhmk-zustand-lens) — Lens support for Zustand. - [@hpkv/zustand-multiplayer](https://github.com/hpkv-io/zustand-multiplayer/tree/main/packages/zustand-multiplayer) — HPKV multiplayer middleware for building realtime collaborative applications - [@liveblocks/zustand](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-zustand) — Liveblocks middleware to make your application multiplayer. - [@prncss-xyz/zustand-optics](https://github.com/prncss-xyz/zustand-optics) — An adapter for [optics-ts](https://github.com/akheron/optics-ts). - [auto-zustand-selectors-hook](https://github.com/Albert-Gao/auto-zustand-selectors-hook) — Automatic generation of Zustand hooks with Typescript support. - [derive-zustand](https://github.com/zustandjs/derive-zustand) — A function to create a derived Zustand store from other Zustand stores. - [geschichte](https://github.com/BowlingX/geschichte) — Zustand and Immer-based hook to manage query parameters. - [leiten-zustand](https://github.com/hecmatyar/leiten-zustand) — Cleans your store from boilerplate for requests and data transformation. - [leo-query](https://github.com/steaks/leo-query) — A simple library to connect async queries to Zustand stores. - [mobz](https://github.com/2A5F/Mobz) — Zustand-style MobX API. - [ngx-zustand](https://github.com/JoaoPauloLousada/ngx-zustand) — A Zustand adapter for Angular. - [persist-and-sync](https://github.com/mayank1513/persist-and-sync) — Zustand middleware to easily persist and sync Zustand state between tabs/windows/iframes with same origin. - [shared-zustand](https://github.com/Tom-Julux/shared-zustand) — Cross-tab state sharing for Zustand. - [simple-zustand-devtools](https://github.com/beerose/simple-zustand-devtools) — 🐻⚛️ Inspect your Zustand store in React DevTools. - [solid-zustand](https://github.com/wobsoriano/solid-zustand) — State management in Solid using Zustand. - [treeshakable](https://github.com/react18-tools/treeshakable) — A wrapper for library creators to avoid redundant store creation. - [use-broadcast-ts](https://github.com/Romainlg29/use-broadcast) — Zustand middleware to share state between tabs. - [use-post-message-ts](https://github.com/paulschoen/use-post-message) — Zustand middleware for sharing state between cross-origin iframes via postMessage browser method. - [use-zustand](https://github.com/zustandjs/use-zustand) — Another custom hook to use Zustand vanilla store. - [vue-zustand](https://github.com/wobsoriano/vue-zustand) — State management solution for Vue based on Zustand. - [zoov](https://github.com/InfiniteXyy/zoov) — State management solution based on Zustand with Module-like API. - [zubridge](https://github.com/goosewobbler/zubridge) — Use Zustand in cross-platform apps, seamlessly. Supports Electron & Tauri. - [zukeeper](https://github.com/oslabs-beta/Zukeeper) — Native devtools with state and action tracking, diffing, tree display, and time travel - [zundo](https://github.com/charkour/zundo) — 🍜 Undo and redo middleware for Zustand, enabling time-travel in your apps. - [zustand-ards](https://github.com/ivoilic/zustand-ards) — 💁 Simple opinionated utilities for example alternative selector formats and default shallow hooks - [zustand-async-slice](https://github.com/mym0404/zustand-async-slice) — Simple Zustand utility to create Async Slice. TypeScript Fully Supported 🖖 - [zustand-boilerplate](https://github.com/sagiereder/zustand-boilerplate) — A tool that automatically generates getters, setters and more for your zustand store. - [zustand-computed](https://github.com/chrisvander/zustand-computed) — A Zustand middleware to create computed states. - [zustand-computed-state](https://github.com/yasintz/zustand-computed-state) — Simple middleware to add computed states. - [zustand-constate](https://github.com/ntvinhit/zustand-constate) — Context-based state management based on Zustand and taking ideas from Constate. - [zustand-context](https://github.com/fredericoo/zustand-context) — Create a zustand store in React Context, containing an initial value, or use it in your components with isolated, mockable instances. - [zustand-create-setter-fn](https://www.npmjs.com/package/zustand-create-setter-fn) — A fully type safe utility for Zustand that allows you to easily update state using React style `setState` functions (framework agnostic, doesn't require React). - [zustand-di](https://github.com/charkour/zustand-di) — use react props to init zustand stores - [zustand-forms](https://github.com/Conduct/zustand-forms) — Fast, type safe form states as Zustand stores. - [zustand-hash-storage](https://github.com/MartinGamesCZ/zustand-hash-storage) — Zustand middleware for saving state into URL hash, b64 encoded (can be configured) and debounce timer. - [zustand-injectors](https://github.com/zustandjs/zustand-injectors) — A sweet way to lazy load slices - [zustand-interval-persist](https://www.npmjs.com/package/zustand-interval-persist) — An enhancement for zustand that enables automatic saving of the store's state to the specified storage at regular interval. - [zustand-lit](https://github.com/ennjin/zustand-lit) — A zustand adapter for lit.js (LitElement) - [zustand-middleware-computed-state](https://github.com/cmlarsen/zustand-middleware-computed-state) — A dead simple middleware for adding computed state to Zustand. - [zustand-middleware-xstate](https://github.com/biowaffeln/zustand-middleware-xstate) — A middleware for putting XState state machines into a global Zustand store. - [zustand-middleware-yjs](https://github.com/joebobmiles/zustand-middleware-yjs) — A middleware for synchronizing Zustand stores with Yjs. - [zustand-mmkv-storage](https://github.com/1mehdifaraji/zustand-mmkv-storage) — Fast, lightweight MMKV storage adapter for Zustand persist middleware in React Native. - [zustand-multi-persist](https://github.com/mooalot/zustand-multi-persist) — A middleware for persisting and rehydrating state to multiple storage engines. - [zustand-mutable](https://github.com/zustandjs/zustand-mutable) — A sweet way to use immer-like mutable updates. - [zustand-namespaces](https://github.com/mooalot/zustand-namespaces) — One store to rule them all. Namespaced Zustand stores. - [zustand-persist](https://github.com/roadmanfong/zustand-persist) — A middleware for persisting and rehydrating state. - [zustand-pub](https://github.com/AwesomeDevin/zustand-pub) — Cross-Application/Cross-Framework State Management And Sharing based on zustand and zustand-vue for React/Vue. - [zustand-querystring](https://github.com/nitedani/zustand-querystring) — A Zustand middleware that syncs the store with the querystring. - [zustand-rx](https://github.com/patdx/zustand-rx) — A Zustand middleware enabling you to subscribe to a store as an RxJS Observable. - [zustand-saga](https://github.com/Nowsta/zustand-saga) — A Zustand middleware for redux-saga (minus redux). - [zustand-slices](https://github.com/zustandjs/zustand-slices) — A slice utility for Zustand. - [zustand-store-addons](https://github.com/Diablow/zustand-store-addons) — React state management addons for Zustand. - [zustand-sync-tabs](https://github.com/mayank1513/zustand-sync-tabs) — Zustand middleware to easily sync Zustand state between tabs/windows/iframes with same origin. - [zustand-utils](https://www.npmjs.com/package/zustand-utils) — Utilities for Zustand — a `createContext` replacement, a devtools wrapper, and a store-updater factory function. - [zustand-valtio](https://github.com/zustandjs/zustand-valtio) — A sweet combination of Zustand and Valtio - [zustand-vue](https://github.com/AwesomeDevin/zustand-vue) — State management for vue (Vue3 / Vue2) based on zustand. - [zustand-x](https://github.com/udecode/zustand-x) — Zustand store factory for a best-in-class developer experience. - [zustand-xs](https://github.com/zustandjs/zustand-xs) — XState/store compabile middleware for Zustand - [zustand-yjs](https://github.com/tandem-pt/zustand-yjs) — Zustand stores for Yjs structures. - [zusteller](https://github.com/timkindberg/zusteller) — Your global state savior. "Just hooks" + Zustand. - [zustorm](https://github.com/mooalot/zustorm) — A simple and powerful form library for Zustand. - [zusty](https://github.com/oslabs-beta/Zusty) — Zustand tool to assist debugging with time travel, action logs, state snapshots, store view, render time metrics and state component tree. ]]> ({ count: 0, text: 'hello', // ... })) const Component = () => { const { count, text } = useCountStore( (state) => ({ count: state.count, text: state.text, }), shallow, ) // ... } ``` That can be done with `createWithEqualityFn` in v5: ```bash npm install use-sync-external-store ``` ```js // v5 import { createWithEqualityFn as create } from 'zustand/traditional' // The rest is the same as v4 ``` Alternatively, for the `shallow` use case, you can use `useShallow` hook: ```js // v5 import { create } from 'zustand' import { useShallow } from 'zustand/shallow' const useCountStore = create((set) => ({ count: 0, text: 'hello', // ... })) const Component = () => { const { count, text } = useCountStore( useShallow((state) => ({ count: state.count, text: state.text, })), ) // ... } ``` ### Requiring stable selector outputs There is a behavioral change in v5 to match React default behavior. If a selector returns a new reference, it may cause infinite loops. For example, this may cause infinite loops. ```js // v4 const [searchValue, setSearchValue] = useStore((state) => [ state.searchValue, state.setSearchValue, ]) ``` The error message will be something like this: ```plaintext Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. ``` To fix it, use the `useShallow` hook, which will return a stable reference. ```js // v5 import { useShallow } from 'zustand/shallow' const [searchValue, setSearchValue] = useStore( useShallow((state) => [state.searchValue, state.setSearchValue]), ) ``` Here's another example that may cause infinite loops. ```js // v4 const action = useMainStore((state) => { return state.action ?? () => {} }) ``` To fix it, make sure the selector function returns a stable reference. ```js // v5 const FALLBACK_ACTION = () => {} const action = useMainStore((state) => { return state.action ?? FALLBACK_ACTION }) ``` Alternatively, if you need v4 behavior, `createWithEqualityFn` will do. ```js // v5 import { createWithEqualityFn as create } from 'zustand/traditional' ``` ### Stricter types when setState's replace flag is set (Typescript only) ```diff - setState: - (partial: T | Partial | ((state: T) => T | Partial), replace?: boolean | undefined) => void; + setState: + (partial: T | Partial | ((state: T) => T | Partial), replace?: false) => void; + (state: T | ((state: T) => T), replace: true) => void; ``` If you are not using the `replace` flag, no migration is required. If you are using the `replace` flag and it's set to `true`, you must provide a complete state object. This change ensures that `store.setState({}, true)` (which results in an invalid state) is no longer considered valid. **Examples:** ```ts // Partial state update (valid) store.setState({ key: 'value' }) // Complete state replacement (valid) store.setState({ key: 'value' }, true) // Incomplete state replacement (invalid) store.setState({}, true) // Error ``` #### Handling Dynamic `replace` Flag If the value of the `replace` flag is dynamic and determined at runtime, you might face issues. To handle this, you can use a workaround by annotating the `replace` parameter with the parameters of the `setState` function: ```ts const replaceFlag = Math.random() > 0.5 const args = [{ bears: 5 }, replaceFlag] as Parameters< typeof useBearStore.setState > store.setState(...args) ``` #### Persist middleware no longer stores item at store creation Previously, the `persist` middleware stored the initial state during store creation. This behavior has been removed in v5 (and v4.5.5). For example, in the following code, the initial state is stored in the storage. ```js // v4 import { create } from 'zustand' import { persist } from 'zustand/middleware' const useCountStore = create( persist( () => ({ count: Math.floor(Math.random() * 1000), }), { name: 'count', }, ), ) ``` In v5, this is no longer the case, and you need to explicitly set the state after store creation. ```js // v5 import { create } from 'zustand' import { persist } from 'zustand/middleware' const useCountStore = create( persist( () => ({ count: 0, }), { name: 'count', }, ), ) useCountStore.setState({ count: Math.floor(Math.random() * 1000), }) ``` ## Links - https://github.com/pmndrs/zustand/pull/2138 - https://github.com/pmndrs/zustand/pull/2580 ]]> ["set"] - , StoreGetState = StoreApi["get"] - , Store = StoreApi - > - (f: ...) => ... + create: + { (): (f: ...) => ... + , (f: ...) => ... + } ``` **Migration** If you are not passing any type parameters to `create`, no migration is required. If you are using a "leaf" middleware like `combine` or `redux`, remove all type parameters from `create`. Else, replace `create(...)` with `create()(...)`. ## `StateCreator` **Applicable imports** ```ts import type { StateCreator } from 'zustand' import type { StateCreator } from 'zustand/vanilla' ``` **Change** ```diff - type StateCreator - < State - , StoreSetState = StoreApi["set"] - , StoreGetState = StoreApi["get"] - , Store = StoreApi - > = - ... + type StateCreator + < State + , InMutators extends [StoreMutatorIdentifier, unknown][] = [] + , OutMutators extends [StoreMutatorIdentifier, unknown][] = [] + , Return = State + > = + ... ``` **Migration** If you are using `StateCreator`, you are likely authoring a middleware or using the "slices" pattern. For that check the [Authoring middlewares and advanced usage](../../learn/guides/advanced-typescript.md#authoring-middlewares-and-advanced-usage) and [Common recipes](../../learn/guides/advanced-typescript.md#common-recipes) sections of the TypeScript Guide. ## `PartialState` **Applicable imports** ```ts import type { PartialState } from 'zustand' import type { PartialState } from 'zustand/vanilla' ``` **Change** ```diff - type PartialState - < T extends State - , K1 extends keyof T = keyof T - , K2 extends keyof T = K1 - , K3 extends keyof T = K2 - , K4 extends keyof T = K3 - > = - | (Pick | Pick | Pick | Pick | T) - | ((state: T) => Pick | Pick | Pick | Pick | T) + type PartialState = + | Partial + | ((state: T) => Partial) ``` **Migration** Replace `PartialState` with `PartialState` and preferably turn on [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) in your `tsconfig.json`: ```json { "compilerOptions": { "exactOptionalPropertyTypes": true } } ``` We're no longer using the trick to disallow `{ foo: undefined }` to be assigned to `Partial<{ foo: string }>`. Instead, we're relying on the users to turn on `exactOptionalPropertyTypes`. ## `useStore` **Applicable imports** ```ts import { useStore } from 'zustand' import { useStore } from 'zustand/react' ``` **Change** ```diff - useStore: - { (store: StoreApi): State - , - ( store: StoreApi - , selector: StateSelector, - , equals?: EqualityChecker - ): StateSlice - } + useStore: + > + ( store: Store + , selector?: StateSelector, + , equals?: EqualityChecker + ) + => StateSlice ``` **Migration** If you are not passing any type parameters to `useStore`, no migration is required. If you are, it's recommended to remove all the type parameters, or pass the **store** type instead of the **state** type as the first parameter. ## `UseBoundStore` **Applicable imports** ```ts import type { UseBoundStore } from 'zustand' import type { UseBoundStore } from 'zustand/react' ``` **Change** ```diff - type UseBoundStore< - State, - Store = StoreApi - > = - & { (): T - , - ( selector: StateSelector - , equals?: EqualityChecker - ): U - } - & Store + type UseBoundStore = + & (> + ( selector?: (state: ExtractState) => StateSlice + , equals?: (a: StateSlice, b: StateSlice) => boolean + ) => StateSlice + ) + & S ``` **Migration** Replace `UseBoundStore` with `UseBoundStore>`, and `UseBoundStore` with `UseBoundStore` ## `UseContextStore` **Applicable imports** ```ts import type { UseContextStore } from 'zustand/context' ``` **Change** ```diff - type UseContextStore ``` **Migration** Use `typeof MyContext.useStore` instead ## `createContext` **Applicable imports** ```ts import createContext from 'zustand/context' ``` **Change** ```diff createContext: - >() => ... + () => ... ``` **Migration** Replace `createContext()` with `createContext>()`, and `createContext()` with `createContext()`. ## `combine`, `devtools`, `subscribeWithSelector` **Applicable imports** ```ts import { combine } from 'zustand/middleware' import { devtools } from 'zustand/middleware' import { subscribeWithSelector } from 'zustand/middleware' ``` **Change** ```diff - combine: - (...) => ... + combine: + (...) => ... - devtools: - (...) => ... + devtools: + (...) => ... - subscribeWithSelector: - (...) => ... + subscribeWithSelector: + (...) => ... ``` **Migration** If you are not passing any type parameters to `combine`, `devtools`, or `subscribeWithSelector`, no migration is required. If you are, remove all the type parameters, as they are inferred automatically. ## `persist` **Applicable imports** ```ts import { persist } from 'zustand/middleware' ``` **Change** ```diff - persist: - >(...) => ... + persist: + (...) => ... ``` **Migration** If you are passing any type parameters, remove them as they are inferred automatically. Next, if you are passing the `partialize` option, there is no further steps required for migration. If you are **not** passing the `partialize` option, you might see some compilation errors. If you do not see any, there is no further migration required. The type of partialized state is now `T` instead of `Partial`, which aligns with the runtime behavior of the default `partialize`, which is an identity (`s => s`). If you see some compilation errors, you have to find and fix the errors yourself, because they might be indicative of unsound code. Alternatively, the workaround will be passing `s => s as Partial` to `partialize`. If your partialized state is truly `Partial`, you should not encounter any bugs. The runtime behavior has not changed, only the types are now correct. ## `redux` **Applicable imports** ```ts import { redux } from 'zustand/middleware' ``` **Change** ```diff - redux: - (...) => ... + redux: + (...) => ... ``` **Migration** If you are not passing any type parameters to `redux`, no migration is required. If you are, remove all the type parameters, and annotate only the second (action) parameter. That is, replace `redux((state, action) => ..., ...)` with `redux((state, action: A) => ..., ...)`. ]]> **Note**: This function is deprecated in v4 and will be removed in v5. See [Migration](#migration). ```jsx import create from 'zustand' import createContext from 'zustand/context' const { Provider, useStore } = createContext() const createStore = () => create(...) const App = () => ( ... ) const Component = () => { const state = useStore() const slice = useStore(selector) ... ``` ## createContext usage in real components ```jsx import create from "zustand"; import createContext from "zustand/context"; // Best practice: You can move the below createContext() and createStore to a separate file(store.js) and import the Provider, useStore here/wherever you need. const { Provider, useStore } = createContext(); const createStore = () => create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }) })); const Button = () => { return ( {/** store() - This will create a store for each time using the Button component instead of using one store for all components **/} ); }; const ButtonChild = () => { const state = useStore(); return (
{state.bears}
); }; export default function App() { return (
); } ``` ## createContext usage with initialization from props ```tsx import create from 'zustand' import createContext from 'zustand/context' const { Provider, useStore } = createContext() export default function App({ initialBears }) { return ( create((set) => ({ bears: initialBears, increase: () => set((state) => ({ bears: state.bears + 1 })), })) } >