W↓
All docs
🔑
Sign Up/Sign In
zustand.docs.pmnd.rs/apis/ (+2)
Public Link
Jun 6, 2025, 12:35:14 PM - complete - 214.6 kB
Created by:
****ad@vlad.studio
Starting URLs:
https://zustand.docs.pmnd.rs/getting-started/introduction
CSS Selector:
main
Crawl Prefixes:
https://zustand.docs.pmnd.rs/apis/
https://zustand.docs.pmnd.rs/guides/
https://zustand.docs.pmnd.rs/hooks/
## Page: https://zustand.docs.pmnd.rs/getting-started/introduction # Introduction How to use 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. ## Installation Zustand is available as a package on NPM for use: # 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. import { create } from 'zustand' const useStore = 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. function BearCounter() { const bears = useStore((state) => state.bears) return <h1>{bears} bears around here...</h1> } function Controls() { const increasePopulation = useStore((state) => state.increasePopulation) return <button onClick={increasePopulation}>one up</button> } Edit this page Next Tutorial: Tic-Tac-Toe --- ## Page: https://zustand.docs.pmnd.rs/guides/tutorial-tic-tac-toe # Tutorial: Tic-Tac-Toe Building a game ## Building a game You will build a small tic-tac-toe game during this tutorial. This tutorial does assume existing React knowledge. The techniques you'll learn in the tutorial are fundamental to building any React app, and fully understanding it will give you a deep understanding of React and Zustand. 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: 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 ( <button style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0, backgroundColor: '#fff', border: '1px solid #999', outline: 0, borderRadius: 0, fontSize: '1rem', fontWeight: 'bold', }} onClick={onSquareClick} > {value} </button> ) } 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 ( <> <div style={{ marginBottom: '0.5rem' }}>{status}</div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((_, i) => ( <Square key={`square-${i}`} value={squares[i]} onSquareClick={() => handleClick(i)} /> ))} </div> </> ) } 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 ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div style={{ marginLeft: '1rem' }}> <ol> {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return ( <li key={historyIndex}> <button onClick={() => jumpTo(historyIndex)}> {description} </button> </li> ) })} </ol> </div> </div> ) } 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 `<button>` element, styled to look like a square. The button displays the value prop, which can be `'X'`, `'O'`, or `null`, depending on the game's state. When the button is clicked, it triggers the `onSquareClick` function passed in as a prop, allowing the game to respond to user input. Here's the code for the `Square` component: function Square({ value, onSquareClick }) { return ( <button style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0, backgroundColor: '#fff', border: '1px solid #999', outline: 0, borderRadius: 0, fontSize: '1rem', fontWeight: 'bold', }} onClick={onSquareClick} > {value} </button> ) } 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 `<div>` 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: export default function Board() { return ( <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > <Square value="1" /> <Square value="2" /> <Square value="3" /> <Square value="4" /> <Square value="5" /> <Square value="6" /> <Square value="7" /> <Square value="8" /> <Square value="9" /> </div> ) } 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: 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 ( <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} /> ))} </div> ) } `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: 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: <Square key={squareIndex} value={square} onSquareClick={() => 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. 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 ( <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} onSquareClick={() => handleClick(squareIndex)} /> ))} </div> ) } 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: 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`: 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 ( <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} onSquareClick={() => handleClick(squareIndex)} /> ))} </div> ) } ### 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'`): 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: 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: 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 ( <> <div style={{ marginBottom: '0.5rem' }}>{status}</div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} onSquareClick={() => handleClick(squareIndex)} /> ))} </div> </> ) } 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: 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 ( <button style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0, backgroundColor: '#fff', border: '1px solid #999', outline: 0, borderRadius: 0, fontSize: '1rem', fontWeight: 'bold', }} onClick={onSquareClick} > {value} </button> ) } 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 ( <> <div style={{ marginBottom: '0.5rem' }}>{status}</div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} onSquareClick={() => handleClick(squareIndex)} /> ))} </div> </> ) } 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: 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: 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 ( <> <div style={{ marginBottom: '0.5rem' }}>{status}</div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} onSquareClick={() => handleClick(squareIndex)} /> ))} </div> </> ) } export default function Game() { return ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board /> </div> <div style={{ marginLeft: '1rem' }}> <ol>{/* TODO */}</ol> </div> </div> ) } Add some state to the `useGameStore` hook to track the history of moves: 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: 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 ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board /> </div> <div style={{ marginLeft: '1rem' }}> <ol>{/*TODO*/}</ol> </div> </div> ) } 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: 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 currentSquares = history[history.length - 1] function handlePlay(nextSquares) { // TODO } return ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div style={{ marginLeft: '1rem' }}> <ol>{/*TODO*/}</ol> </div> </div> ) } 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. 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 ( <> <div style={{ marginBottom: '0.5rem' }}>{status}</div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} onSquareClick={() => handleClick(squareIndex)} /> ))} </div> </> ) } 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. 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: 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 ( <button style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0, backgroundColor: '#fff', border: '1px solid #999', outline: 0, borderRadius: 0, fontSize: '1rem', fontWeight: 'bold', }} onClick={onSquareClick} > {value} </button> ) } 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 ( <> <div style={{ marginBottom: '0.5rem' }}>{status}</div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)', width: 'calc(3 * 2.5rem)', height: 'calc(3 * 2.5rem)', border: '1px solid #999', }} > {squares.map((square, squareIndex) => ( <Square key={squareIndex} value={square} onSquareClick={() => handleClick(squareIndex)} /> ))} </div> </> ) } 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 ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div style={{ marginLeft: '1rem' }}> <ol>{/*TODO*/}</ol> </div> </div> ) } 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: 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 ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div style={{ marginLeft: '1rem' }}> <ol> {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return ( <li key={historyIndex}> <button onClick={() => jumpTo(historyIndex)}> {description} </button> </li> ) })} </ol> </div> </div> ) } 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`: 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. 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. 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: 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 ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div style={{ marginLeft: '1rem' }}> <ol> {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return ( <li key={historyIndex}> <button onClick={() => jumpTo(historyIndex)}> {description} </button> </li> ) })} </ol> </div> </div> ) } ### 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`: 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 ( <div style={{ display: 'flex', flexDirection: 'row', fontFamily: 'monospace', }} > <div> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div style={{ marginLeft: '1rem' }}> <ol> {history.map((_, historyIndex) => { const description = historyIndex > 0 ? `Go to move #${historyIndex}` : 'Go to game start' return ( <li key={historyIndex}> <button onClick={() => jumpTo(historyIndex)}> {description} </button> </li> ) })} </ol> </div> </div> ) } 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. Edit this page Previous Introduction Next Comparison --- ## Page: https://zustand.docs.pmnd.rs/apis/create-store # createStore How to create vanilla stores `createStore` lets you create a vanilla store that exposes API utilities. const someStore = createStore(stateCreatorFn) * Types * Signature * Reference * Usage * Updating state based on previous state * Updating Primitives in State * Updating Objects in State * Updating Arrays in State * Subscribing to state updates * Troubleshooting * I’ve updated the state, but the screen doesn’t update ## Types ### Signature createStore<T>()(stateCreatorFn: StateCreator<T, [], []>): StoreApi<T> ## 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**. 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<AgeStore>()((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<typeof ageStore.subscribe>[0] = (state) => { $yourAgeHeading.innerHTML = `Your age: ${state.age}` } render(ageStore.getInitialState(), ageStore.getInitialState()) ageStore.subscribe(render) Here's the `html` code <h1 id="your-age"></h1> <button id="increment-by-3" type="button">+3</button> <button id="increment-by-1" type="button">+1</button> ### 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` import { createStore } from 'zustand/vanilla' type XStore = number const xStore = createStore<XStore>()(() => 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<typeof xStore.subscribe>[0] = (x) => { $dot.style.transform = `translate(${x}px, 0)` } render(xStore.getInitialState(), xStore.getInitialState()) xStore.subscribe(render) Here's the `html` code <div id="dot-container" style="position: relative; width: 100vw; height: 100vh;" > <div id="dot" style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;" ></div> </div> ### 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. 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<PositionStore>()((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<typeof positionStore.subscribe>[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 <div id="dot-container" style="position: relative; width: 100vw; height: 100vh;" > <div id="dot" style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;" ></div> </div> ### 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(...)`. import { createStore } from 'zustand/vanilla' type PositionStore = [number, number] const positionStore = createStore<PositionStore>()(() => [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<typeof positionStore.subscribe>[0] = ([x, y]) => { $dot.style.transform = `translate(${x}px, ${y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render) Here's the `html` code <div id="dot-container" style="position: relative; width: 100vw; height: 100vh;" > <div id="dot" style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;" ></div> </div> ### 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. 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<PositionStore>()((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<typeof positionStore.subscribe>[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<typeof positionStore.subscribe>[0] = (state) => { console.log('new position', { position: state.position }) } positionStore.subscribe(logger) Here's the `html` code <div id="dot-container" style="position: relative; width: 100vw; height: 100vh;" > <div id="dot" style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;" ></div> </div> ## 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: 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<PersonStore>()((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<typeof personStore.subscribe>[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 <label style="display: block"> First name: <input id="first-name" /> </label> <label style="display: block"> Last name: <input id="last-name" /> </label> <label style="display: block"> Email: <input id="email" /> </label> <p id="result"></p> For example, this line mutates the state from a past render: 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: 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! 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<PersonStore>()((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<typeof personStore.subscribe>[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) Edit this page Previous How to Migrate to v5 from v4 Next createWithEqualityFn ⚛️ --- ## Page: https://zustand.docs.pmnd.rs/hooks/use-shallow # useShallow ⚛️ How to memoize selector functions `useShallow` is a React Hook that lets you optimize re-renders. const memoizedSelector = useShallow(selector) * Types * Signature * Reference * Usage * Writing a memoized selector * Troubleshooting ### Signature useShallow<T, U = T>(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. import { create } from 'zustand' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create<BearFamilyMealsStore>()(() => ({ 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. function BearNames() { const names = useBearFamilyMealsStore((state) => Object.keys(state)) return <div>{names.join(', ')}</div> } Next, we will create a `UpdateBabyBearMeal` component that periodically updates baby bear's meal choice. 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. export default function App() { return ( <> <UpdateBabyBearMeal /> <BearNames /> </> ) } Here is what the code should look like: import { useEffect } from 'react' import { create } from 'zustand' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create<BearFamilyMealsStore>()(() => ({ 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 <div>{names.join(', ')}</div> } export default function App() { return ( <> <UpdateBabyBearMeal /> <BearNames /> </> ) } 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: function BearNames() { const names = useBearFamilyMealsStore( useShallow((state) => Object.keys(state)), ) return <div>{names.join(', ')}</div> } Here is what the code should look like: import { useEffect } from 'react' import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' type BearFamilyMealsStore = { [key: string]: string } const useBearFamilyMealsStore = create<BearFamilyMealsStore>()(() => ({ 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 <div>{names.join(', ')}</div> } export default function App() { return ( <> <UpdateBabyBearMeal /> <BearNames /> </> ) } By using `useShallow`, we optimized the rendering process, ensuring that the component only re-renders when necessary, which improves overall performance. ## Troubleshooting TBD Edit this page Previous shallow Next useStoreWithEqualityFn ⚛️ --- ## Page: https://zustand.docs.pmnd.rs/guides/updating-state # Updating state ## Flat updates Updating state with Zustand is simple! Call the provided `set` function with the new state, and it will be shallowly merged with the existing state in the store. **Note** See next section for nested state. import { create } from 'zustand' type State = { firstName: string lastName: string } type Action = { updateFirstName: (firstName: State['firstName']) => void updateLastName: (lastName: State['lastName']) => void } // Create your store, which includes both state and (optionally) actions const usePersonStore = create<State & Action>((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 ( <main> <label> First name <input // Update the "firstName" state onChange={(e) => updateFirstName(e.currentTarget.value)} value={firstName} /> </label> <p> Hello, <strong>{firstName}!</strong> </p> </main> ) } ## Deeply nested object If you have a deep state object like this: 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: 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 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: immerInc: () => set(produce((state: State) => { ++state.deep.nested.obj.count })), What a reduction! Please take note of the gotchas listed here. ### With optics-ts There is another option with optics-ts: opticsInc: () => set(O.modify(O.optic<State>().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: ramdaInc: () => set(R.modifyPath(["deep", "nested", "obj", "count"], (c) => c + 1)), Both ramda and optics-ts also work with types. ### CodeSandbox Demo https://codesandbox.io/s/zustand-normal-immer-optics-ramda-updating-ynn3o?file=/src/App.tsx Edit this page Previous Comparison Next Immutable state and merging --- ## Page: https://zustand.docs.pmnd.rs/guides/immutable-state-and-merging # Immutable state and merging Like with React's `useState`, we need to update state immutably. Here's a typical example: import { create } from 'zustand' const useCountStore = create((set) => ({ 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: 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: 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: 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. ## Replace flag To disable the merging behavior, you can specify a `replace` boolean value for `set` like so: set((state) => newState, true) Edit this page Previous Updating state Next Flux inspired practice --- ## Page: https://zustand.docs.pmnd.rs/guides/flux-inspired-practice # Flux inspired practice Although Zustand is an unopinionated library, we do recommend a few patterns. These are inspired by practices originally found in Flux, and more recently Redux, so if you are coming from another library, you should feel right at home. However, Zustand does differ in some fundamental ways, so some terminology may not perfectly align to other libraries. ## Recommended patterns ### Single store Your applications global state should be located in a single Zustand store. If you have a large application, Zustand supports splitting the store into slices. ### Use `set` / `setState` to update the store Always use `set` (or `setState`) to perform updates to your store. `set` (and `setState`) ensures the described update is correctly merged and listeners are appropriately notified. ### Colocate store actions In Zustand, state can be updated without the use of dispatched actions and reducers found in other Flux libraries. These store actions can be added directly to the store as shown below. Optionally, by using `setState` they can be located external to the store const useBoundStore = create((set) => ({ 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: 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. 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. Edit this page Previous Immutable state and merging Next Auto Generating Selectors --- ## Page: https://zustand.docs.pmnd.rs/guides/auto-generating-selectors # Auto Generating Selectors We recommend using selectors when using either the properties or actions from the store. You can access values from the store like so: const bears = useBearStore((state) => 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` import { StoreApi, UseBoundStore } from 'zustand' type WithSelectors<S> = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never const createSelectors = <S extends UseBoundStore<StoreApi<object>>>( _store: S, ) => { const store = _store as WithSelectors<typeof _store> 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: interface BearState { bears: number increase: (by: number) => void increment: () => void } const useBearStoreBase = create<BearState>()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), increment: () => set((state) => ({ bears: state.bears + 1 })), })) Apply that function to your store: const useBearStore = createSelectors(useBearStoreBase) Now the selectors are auto generated and you can access them directly: // 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: import { StoreApi, useStore } from 'zustand' type WithSelectors<S> = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never const createSelectors = <S extends StoreApi<object>>(_store: S) => { const store = _store as WithSelectors<typeof _store> 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: import { createStore } from 'zustand' interface BearState { bears: number increase: (by: number) => void increment: () => void } const store = createStore<BearState>((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), increment: () => set((state) => ({ bears: state.bears + 1 })), })) Apply that function to your store: const useBearStore = createSelectors(store) Now the selectors are auto generated and you can access them directly: // 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. ## Third-party Libraries * auto-zustand-selectors-hook * react-hooks-global-state * zustood * @davstack/store Edit this page Previous Flux inspired practice Next Practice with no store actions --- ## Page: https://zustand.docs.pmnd.rs/guides/practice-with-no-store-actions # Practice with no store actions The recommended usage is to colocate actions and states within the store (let your actions be located together with your state). For example: export const useBoundStore = create((set) => ({ 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. 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. Edit this page Previous Auto Generating Selectors Next TypeScript Guide --- ## Page: https://zustand.docs.pmnd.rs/guides/typescript # TypeScript Guide ## Basic usage The difference when using TypeScript is that instead of writing `create(...)`, you have to write `create<T>()(...)` (notice the extra parentheses `()` too along with the type parameter) where `T` is the type of the state to annotate it. For example: import { create } from 'zustand' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create<BearState>()((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`: declare const create: <T>(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: const createFoo = {} as <T>(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 `<T>(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: 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: 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. Imagine you have a scenario like this: declare const withError: <T, E>( p: Promise<T>, ) => Promise<[error: undefined, value: T] | [error: E, value: undefined]> declare const doSomething: () => Promise<string> 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`. declare const withError: { <E>(): <T>( p: Promise<T>, ) => Promise<[error: undefined, value: T] | [error: E, value: undefined]> <T, E>( p: Promise<T>, ): Promise<[error: undefined, value: T] | [error: E, value: undefined]> } declare const doSomething: () => Promise<string> interface Foo { bar: string } const main = async () => { let [error, value] = await withError<Foo>()(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. 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: import { create, ExtractState } from 'zustand' import { combine } from 'zustand/middleware' type BearState = ExtractState<typeof useBearStore> 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. import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create<BearState>()( 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. 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<BearState>()( 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. 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. If you are eager to know what the answer is to this particular problem then you can see it here. ### 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: const replaceFlag = Math.random() > 0.5 const args = [{ bears: 5 }, replaceFlag] as Parameters< typeof useBearStore.setState > store.setState(...args) #### Example with `as Parameters` Workaround import { create } from 'zustand' interface BearState { bears: number increase: (by: number) => void } const useBearStore = create<BearState>()((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 import { create, StateCreator, StoreMutatorIdentifier } from 'zustand' type Logger = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( f: StateCreator<T, Mps, Mcs>, name?: string, ) => StateCreator<T, Mps, Mcs> type LoggerImpl = <T>( f: StateCreator<T, [], []>, name?: string, ) => StateCreator<T, [], []> const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => { const loggedSet: typeof set = (...a) => { set(...(a as Parameters<typeof set>)) console.log(...(name ? [`${name}:`] : []), get()) } const setState = store.setState store.setState = (...a) => { setState(...(a as Parameters<typeof setState>)) console.log(...(name ? [`${name}:`] : []), store.getState()) } return f(loggedSet, get, store) } export const logger = loggerImpl as unknown as Logger // --- const useBearStore = create<BearState>()( logger( (set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), }), 'bear-store', ), ) ### Middleware that changes the store type import { create, StateCreator, StoreMutatorIdentifier, Mutate, StoreApi, } from 'zustand' type Foo = < T, A, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], >( f: StateCreator<T, [...Mps, ['foo', A]], Mcs>, bar: A, ) => StateCreator<T, Mps, [['foo', A], ...Mcs]> declare module 'zustand' { interface StoreMutators<S, A> { foo: Write<Cast<S, object>, { foo: A }> } } type FooImpl = <T, A>( f: StateCreator<T, [], []>, bar: A, ) => StateCreator<T, [], []> const fooImpl: FooImpl = (f, bar) => (set, get, _store) => { type T = ReturnType<typeof f> type A = typeof bar const store = _store as Mutate<StoreApi<T>, [['foo', A]]> store.foo = bar return f(set, get, _store) } export const foo = fooImpl as unknown as Foo type Write<T extends object, U extends object> = Omit<T, keyof U> & U type Cast<T, U> = 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<T>()(...)`. 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. 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 import { create, StateCreator } from 'zustand' interface BearSlice { bears: number addBear: () => void eatFish: () => void } interface FishSlice { fishes: number addFish: () => void } interface SharedSlice { addBoth: () => void getBoth: () => void } 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<BearSlice & FishSlice & SharedSlice>()((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createSharedSlice(...a), })) A detailed explanation on the slices pattern can be found here. If you have some middlewares then replace `StateCreator<MyState, [], [], MySlice>` with `StateCreator<MyState, Mutators, [], MySlice>`. For example, if you are using `devtools` then it will be `StateCreator<MyState, [["zustand/devtools", never]], [], MySlice>`. See the "Middlewares and their mutators reference" section for a list of all mutators. ### Bounded `useStore` hook for vanilla stores import { useStore } from 'zustand' import { createStore } from 'zustand/vanilla' interface BearState { bears: number increase: (by: number) => void } const bearStore = createStore<BearState>()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) function useBearStore(): BearState function useBearStore<T>(selector: (state: BearState) => T): T function useBearStore<T>(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... import { useStore, StoreApi } from 'zustand' import { createStore } from 'zustand/vanilla' interface BearState { bears: number increase: (by: number) => void } const bearStore = createStore<BearState>()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })) const createBoundedUseStore = ((store) => (selector) => useStore(store, selector)) as <S extends StoreApi<unknown>>( store: S, ) => { (): ExtractState<S> <T>(selector: (state: ExtractState<S>) => T): T } type ExtractState<S> = 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<YourState>`. Also sometimes 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 Edit this page Previous Practice with no store actions Next Testing --- ## Page: https://zustand.docs.pmnd.rs/guides/testing # Testing Writing Tests ## Setting Up a Test Environment ### Test Runners Usually, your test runner needs to be configured to run JavaScript/TypeScript syntax. If you're going to be testing UI components, you will likely need to configure the test runner to use JSDOM to provide a mock DOM environment. See these resources for test runner configuration instructions: * **Jest** * Jest: Getting Started * Jest: Configuration - Test Environment * **Vitest** * Vitest: Getting Started * Vitest: Configuration - Test Environment ### UI and Network Testing Tools **We recommend using React Testing Library (RTL) to test out React components that connect to Zustand**. RTL is a simple and complete React DOM testing utility that encourages good testing practices. It uses ReactDOM's `render` function and `act` from `react-dom/tests-utils`. Furthermore, Native Testing Library (RNTL) is the alternative to RTL to test out React Native components. The Testing Library family of tools also includes adapters for many other popular frameworks. We also recommend using Mock Service Worker (MSW) to mock network requests, as this means your application logic does not need to be changed or mocked when writing tests. * **React Testing Library (DOM)** * DOM Testing Library: Setup * React Testing Library: Setup * Testing Library Jest-DOM Matchers * **Native Testing Library (React Native)** * Native Testing Library: Setup * **User Event Testing Library (DOM)** * User Event Testing Library: Setup * **TypeScript for Jest** * TypeScript for Jest: Setup * **TypeScript for Node** * TypeScript for Node: Setup * **Mock Service Worker** * MSW: Installation * MSW: Setting up mock requests * MSW: Mock server configuration for Node ## Setting Up Zustand for testing > **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. // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator<CounterStore> = (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. // __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<typeof ZustandExportedTypes>('zustand') // a variable to hold reset functions for all stores declared in the app export const storeResetFns = new Set<() => void>() const createUncurried = <T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { 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 = (<T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { console.log('zustand create mock') // to support curried version of create return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried }) as typeof ZustandExportedTypes.create const createStoreUncurried = <T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { 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 = (<T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { 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() }) }) }) // setup-jest.ts import '@testing-library/jest-dom' // 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. 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. // __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<typeof ZustandExportedTypes>('zustand') // a variable to hold reset functions for all stores declared in the app export const storeResetFns = new Set<() => void>() const createUncurried = <T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { 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 = (<T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { console.log('zustand create mock') // to support curried version of create return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried }) as typeof ZustandExportedTypes.create const createStoreUncurried = <T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { 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 = (<T>( stateCreator: ZustandExportedTypes.StateCreator<T>, ) => { 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 enabled, we need to add `import { afterEach, vi } from 'vitest'` at the top. // global.d.ts /// <reference types="vite/client" /> /// <reference types="vitest/globals" /> > **Note**: without globals configuration enabled, we do need to remove `/// <reference types="vitest/globals" />`. // setup-vitest.ts import '@testing-library/jest-dom' vi.mock('zustand') // to make it work like Jest (auto-mocking) > **Note**: without globals configuration enabled, we need to add `import { vi } from 'vitest'` at the top. // 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. // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator<CounterStore> = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) // stores/use-counter-store.ts import { create } from 'zustand' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const useCounterStore = create<CounterStore>()(counterStoreCreator) // contexts/use-counter-store-context.tsx import { type ReactNode, createContext, useContext, useRef } 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<CounterStore>(counterStoreCreator) } export type CounterStoreApi = ReturnType<typeof createCounterStore> export const CounterStoreContext = createContext<CounterStoreApi | undefined>( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const counterStoreRef = useRef<CounterStoreApi>(null) if (!counterStoreRef.current) { counterStoreRef.current = createCounterStore() } return ( <CounterStoreContext.Provider value={counterStoreRef.current}> {children} </CounterStoreContext.Provider> ) } export type UseCounterStoreContextSelector<T> = (store: CounterStore) => T export const useCounterStoreContext = <T,>( selector: UseCounterStoreContextSelector<T>, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (counterStoreContext === undefined) { throw new Error( 'useCounterStoreContext must be used within CounterStoreProvider', ) } return useStoreWithEqualityFn(counterStoreContext, selector, shallow) } // components/counter/counter.tsx import { useCounterStore } from '../../stores/use-counter-store' export function Counter() { const { count, inc } = useCounterStore() return ( <div> <h2>Counter Store</h2> <h4>{count}</h4> <button onClick={inc}>One Up</button> </div> ) } // components/counter/index.ts export * from './counter' // 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(<Counter />) } // 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 ( <div> <h2>Counter Store Context</h2> <h4>{count}</h4> <button onClick={inc}>One Up</button> </div> ) } export const CounterWithContext = () => { return ( <CounterStoreProvider> <Counter /> </CounterStoreProvider> ) } // components/counter-with-context/index.ts export * from './counter-with-context' // 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(<CounterWithContext />) } > **Note**: without globals configuration enabled, we need to add `import { describe, test, expect } from 'vitest'` at the top of each test file. **CodeSandbox Demos** * Jest Demo: https://stackblitz.com/edit/jest-zustand * Vitest Demo: https://stackblitz.com/edit/vitest-zustand ### Testing Stores In the next examples we are going to use `useCounterStore` > **Note**: all of these examples are written using TypeScript. // shared/counter-store-creator.ts import { type StateCreator } from 'zustand' export type CounterStore = { count: number inc: () => void } export const counterStoreCreator: StateCreator<CounterStore> = (set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), }) // stores/use-counter-store.ts import { create } from 'zustand' import { type CounterStore, counterStoreCreator, } from '../shared/counter-store-creator' export const useCounterStore = create<CounterStore>()(counterStoreCreator) // contexts/use-counter-store-context.tsx import { type ReactNode, createContext, useContext, useRef } 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<CounterStore>(counterStoreCreator) } export type CounterStoreApi = ReturnType<typeof createCounterStore> export const CounterStoreContext = createContext<CounterStoreApi | undefined>( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const counterStoreRef = useRef<CounterStoreApi>(null) if (!counterStoreRef.current) { counterStoreRef.current = createCounterStore() } return ( <CounterStoreContext.Provider value={counterStoreRef.current}> {children} </CounterStoreContext.Provider> ) } export type UseCounterStoreContextSelector<T> = (store: CounterStore) => T export const useCounterStoreContext = <T,>( selector: UseCounterStoreContextSelector<T>, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (counterStoreContext === undefined) { throw new Error( 'useCounterStoreContext must be used within CounterStoreProvider', ) } return useStoreWithEqualityFn(counterStoreContext, selector, shallow) } // components/counter/counter.tsx import { useCounterStore } from '../../stores/use-counter-store' export function Counter() { const { count, inc } = useCounterStore() return ( <div> <h2>Counter Store</h2> <h4>{count}</h4> <button onClick={inc}>One Up</button> </div> ) } // components/counter/index.ts export * from './counter' // 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(<Counter />) } // 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 ( <div> <h2>Counter Store Context</h2> <h4>{count}</h4> <button onClick={inc}>One Up</button> </div> ) } export const CounterWithContext = () => { return ( <CounterStoreProvider> <Counter /> </CounterStoreProvider> ) } // components/counter-with-context/index.ts export * from './counter-with-context' // 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(<CounterWithContext />, { wrapper: ({ children }) => ( <CounterStoreContext.Provider value={store}> {children} </CounterStoreContext.Provider> ), }) } ## References * **React Testing Library**: React Testing Library (RTL) 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) 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. Edit this page Previous TypeScript Guide Next Calling actions outside a React event handler in pre React 18 --- ## Page: https://zustand.docs.pmnd.rs/guides/event-handler-in-pre-react-18 # Calling actions outside a React event handler in pre React 18 Because React handles `setState` synchronously if it's called outside an event handler, updating the state outside an event handler will force react to update the components synchronously. Therefore, there is a risk of encountering the zombie-child effect. In order to fix this, the action needs to be wrapped in `unstable_batchedUpdates` like so: import { unstable_batchedUpdates } from 'react-dom' // or 'react-native' const useFishStore = create((set) => ({ 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 Edit this page Previous Testing Next Map and Set Usage --- ## Page: https://zustand.docs.pmnd.rs/guides/maps-and-sets-usage # Map and Set Usage You need to wrap Maps and Sets inside an object. When you want its update to be reflected (e.g. in React), you do it by calling `setState` on it: **You can view a codesandbox here: https://codesandbox.io/s/late-https-bxz9qy** import { create } from 'zustand' const useFooBar = create(() => ({ foo: new Map(), bar: new Set() })) function doSomething() { // doing something... // If you want to update some React component that uses `useFooBar`, you have to call setState // to let React know that an update happened. // Following React's best practices, you should create a new Map/Set when updating them: useFooBar.setState((prev) => ({ foo: new Map(prev.foo).set('newKey', 'newValue'), bar: new Set(prev.bar).add('newKey'), })) } Edit this page Previous Calling actions outside a React event handler in pre React 18 Next Connect to state with URL --- ## Page: https://zustand.docs.pmnd.rs/guides/connect-to-state-with-url-hash # Connect to state with URL ## Connect State with URL Hash If you want to connect state of a store to URL hash, you can create your own hash storage. import { create } from 'zustand' import { persist, StateStorage, createJSONStorage } from 'zustand/middleware' const hashStorage: StateStorage = { getItem: (key): string => { 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), }, ), ) ### CodeSandbox Demo https://codesandbox.io/s/zustand-state-with-url-hash-demo-f29b88?file=/src/store/index.ts ## 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. 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<LocalAndUrlStore>(() => persistentStorage), } const useLocalAndUrlStore = create( persist<LocalAndUrlStore>( (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: 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}}` Edit this page Previous Map and Set Usage Next How to reset state --- ## Page: https://zustand.docs.pmnd.rs/guides/how-to-reset-state # How to reset state The following pattern can be used to reset the state to its initial value. import { create } from 'zustand' // define types for state values and actions separately type State = { salmon: number tuna: number } type Actions = { addSalmon: (qty: number) => void addTuna: (qty: number) => void reset: () => void } // define the initial state const initialState: State = { salmon: 0, tuna: 0, } // create store const useSlice = create<State & Actions>()((set, get) => ({ ...initialState, addSalmon: (qty: number) => { set({ salmon: get().salmon + qty }) }, addTuna: (qty: number) => { set({ tuna: get().tuna + qty }) }, reset: () => { set(initialState) }, })) Resetting multiple stores at once import type { StateCreator } from 'zustand' import { create: actualCreate } from 'zustand' const storeResetFns = new Set<() => void>() const resetAllStores = () => { storeResetFns.forEach((resetFn) => { resetFn() }) } export const create = (<T>() => { return (stateCreator: StateCreator<T>) => { const store = actualCreate(stateCreator) const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } }) as typeof actualCreate ## CodeSandbox Demo * Basic: https://codesandbox.io/s/zustand-how-to-reset-state-basic-demo-rrqyon * Advanced: https://codesandbox.io/s/zustand-how-to-reset-state-advanced-demo-gtu0qe * Immer: https://codesandbox.io/s/how-to-reset-state-advance-immer-demo-nyet3f Edit this page Previous Connect to state with URL Next Initialize state with props --- ## Page: https://zustand.docs.pmnd.rs/guides/initialize-state-with-props # Initialize state with props In cases where dependency injection is needed, such as when a store should be initialized with props from a component, the recommended approach is to use a vanilla store with React.context. ## Store creator with `createStore` import { createStore } from 'zustand' interface BearProps { bears: number } interface BearState extends BearProps { addBear: () => void } type BearStore = ReturnType<typeof createBearStore> const createBearStore = (initProps?: Partial<BearProps>) => { const DEFAULT_PROPS: BearProps = { bears: 0, } return createStore<BearState>()((set) => ({ ...DEFAULT_PROPS, ...initProps, addBear: () => set((state) => ({ bears: ++state.bears })), })) } ## Creating a context with `React.createContext` import { createContext } from 'react' export const BearContext = createContext<BearStore | null>(null) ## Basic component usage // Provider implementation import { useRef } from 'react' function App() { const store = useRef(createBearStore()).current return ( <BearContext.Provider value={store}> <BasicConsumer /> </BearContext.Provider> ) } // 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 ( <> <div>{bears} Bears.</div> <button onClick={addBear}>Add bear</button> </> ) } ## Common patterns ### Wrapping the context provider // Provider wrapper import { useRef } from 'react' type BearProviderProps = React.PropsWithChildren<BearProps> function BearProvider({ children, ...props }: BearProviderProps) { const storeRef = useRef<BearStore>() if (!storeRef.current) { storeRef.current = createBearStore(props) } return ( <BearContext.Provider value={storeRef.current}> {children} </BearContext.Provider> ) } ### Extracting context logic into a custom hook // Mimic the hook returned by `create` import { useContext } from 'react' import { useStore } from 'zustand' function useBearContext<T>(selector: (state: BearState) => T): T { const store = useContext(BearContext) if (!store) throw new Error('Missing BearContext.Provider in the tree') return useStore(store, selector) } // Consumer usage of the custom hook function CommonConsumer() { const bears = useBearContext((s) => s.bears) const addBear = useBearContext((s) => s.addBear) return ( <> <div>{bears} Bears.</div> <button onClick={addBear}>Add bear</button> </> ) } ### Optionally allow using a custom equality function // Allow custom equality function by using useStoreWithEqualityFn instead of useStore import { useContext } from 'react' import { useStoreWithEqualityFn } from 'zustand/traditional' function useBearContext<T>( 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 // Provider wrapper & custom hook consumer function App2() { return ( <BearProvider bears={2}> <HookConsumer /> </BearProvider> ) } Edit this page Previous How to reset state Next Slices Pattern --- ## Page: https://zustand.docs.pmnd.rs/guides/slices-pattern # Slices Pattern ## Slicing the store into smaller stores Your store can become bigger and bigger and tougher to maintain as you add more features. You can divide your main store into smaller individual stores to achieve modularity. This is simple to accomplish in Zustand! The first individual store: export const createFishSlice = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) Another individual store: 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**: 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 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 ( <div> <h2>Number of bears: {bears}</h2> <h2>Number of fishes: {fishes}</h2> <button onClick={() => addBear()}>Add a bear</button> </div> ) } export default App ### Updating multiple stores You can update multiple stores, at the same time, in a single function. export const createBearFishSlice = (set, get) => ({ addBearAndFish: () => { get().addBear() get().addFish() }, }) Combining all the stores together is the same as before. 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 to our `useBoundStore`: 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. Edit this page Previous Initialize state with props Next Prevent rerenders with useShallow --- ## Page: https://zustand.docs.pmnd.rs/guides/ssr-and-hydration # SSR and Hydration ## Server-side Rendering (SSR) Server-side Rendering (SSR) is a technique that helps us render our components into HTML strings on the server, send them directly to the browser, and finally "hydrate" the static markup into a fully interactive app on the client. ### React Let's say we want to render a stateless app using React. In order to do that, we need to use `express`, `react` and `react-dom/server`. We don't need `react-dom/client` since it's a stateless app. 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. // tsconfig.json { "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "target": "esnext" }, "include": ["**/*"] } > **Note:** do not forget to remove all comments from your `tsconfig.json` file. // app.tsx export const App = () => { return ( <html> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Static Server-side-rendered App</title> </head> <body> <div>Hello World!</div> </body> </html> ) } // 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(<App />, { onShellReady() { res.setHeader('content-type', 'text/html') pipe(res) }, }) }) app.listen(port, () => { console.log(`Server is listening at ${port}`) }) tsc --build 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. // 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. // app.tsx export const App = () => { return ( <html> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Static Server-side-rendered App</title> </head> <body> <div>Hello World!</div> </body> </html> ) } // main.tsx import ReactDOMClient from 'react-dom/client' import { App } from './app.tsx' ReactDOMClient.hydrateRoot(document, <App />) // 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(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { res.setHeader('content-type', 'text/html') pipe(res) }, }) }) app.listen(port, () => { console.log(`Server is listening at ${port}`) }) tsc --build 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 Edit this page Previous Prevent rerenders with useShallow Next Setup with Next.js --- ## Page: https://zustand.docs.pmnd.rs/guides/nextjs # Setup with Next.js Next.js 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 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. // 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. // 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<CounterStore>()((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. // src/providers/counter-store-provider.tsx 'use client' import { type ReactNode, createContext, useRef, useContext } from 'react' import { useStore } from 'zustand' import { type CounterStore, createCounterStore } from '@/stores/counter-store' export type CounterStoreApi = ReturnType<typeof createCounterStore> export const CounterStoreContext = createContext<CounterStoreApi | undefined>( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const storeRef = useRef<CounterStoreApi | null>(null) if (storeRef.current === null) { storeRef.current = createCounterStore() } return ( <CounterStoreContext.Provider value={storeRef.current}> {children} </CounterStoreContext.Provider> ) } export const useCounterStore = <T,>( 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. ### Initializing the store // 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 initCounterStore = (): CounterState => { return { count: new Date().getFullYear() } } export const defaultInitState: CounterState = { count: 0, } export const createCounterStore = ( initState: CounterState = defaultInitState, ) => { return createStore<CounterStore>()((set) => ({ ...initState, decrementCount: () => set((state) => ({ count: state.count - 1 })), incrementCount: () => set((state) => ({ count: state.count + 1 })), })) } // src/providers/counter-store-provider.tsx 'use client' import { type ReactNode, createContext, useRef, useContext } from 'react' import { useStore } from 'zustand' import { type CounterStore, createCounterStore, initCounterStore, } from '@/stores/counter-store' export type CounterStoreApi = ReturnType<typeof createCounterStore> export const CounterStoreContext = createContext<CounterStoreApi | undefined>( undefined, ) export interface CounterStoreProviderProps { children: ReactNode } export const CounterStoreProvider = ({ children, }: CounterStoreProviderProps) => { const storeRef = useRef<CounterStoreApi | null>(null) if (storeRef.current === null) { storeRef.current = createCounterStore(initCounterStore()) } return ( <CounterStoreContext.Provider value={storeRef.current}> {children} </CounterStoreContext.Provider> ) } export const useCounterStore = <T,>( selector: (store: CounterStore) => T, ): T => { const counterStoreContext = useContext(CounterStoreContext) if (!counterStoreContext) { throw new Error(`useCounterStore must be used within CounterStoreProvider`) } return useStore(counterStoreContext, selector) } ### Using the store with different architectures There are two architectures for a Next.js application: the Pages Router and the App Router. The usage of Zustand on both architectures should be the same with slight differences related to each architecture. #### Pages Router // src/components/pages/home-page.tsx import { useCounterStore } from '@/providers/counter-store-provider.ts' export const HomePage = () => { const { count, incrementCount, decrementCount } = useCounterStore( (state) => state, ) return ( <div> Count: {count} <hr /> <button type="button" onClick={incrementCount}> Increment Count </button> <button type="button" onClick={decrementCount}> Decrement Count </button> </div> ) } // src/_app.tsx import type { AppProps } from 'next/app' import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx' export default function App({ Component, pageProps }: AppProps) { return ( <CounterStoreProvider> <Component {...pageProps} /> </CounterStoreProvider> ) } // src/pages/index.tsx import { HomePage } from '@/components/pages/home-page.tsx' export default function Home() { return <HomePage /> } > **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. // src/pages/index.tsx import { CounterStoreProvider } from '@/providers/counter-store-provider.tsx' import { HomePage } from '@/components/pages/home-page.tsx' export default function Home() { return ( <CounterStoreProvider> <HomePage /> </CounterStoreProvider> ) } #### App Router // 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 ( <div> Count: {count} <hr /> <button type="button" onClick={incrementCount}> Increment Count </button> <button type="button" onClick={decrementCount}> Decrement Count </button> </div> ) } // 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 ( <html lang="en"> <body className={inter.className}> <CounterStoreProvider>{children}</CounterStoreProvider> </body> </html> ) } // src/app/page.tsx import { HomePage } from '@/components/pages/home-page' export default function Home() { return <HomePage /> } > **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. // src/app/page.tsx import { CounterStoreProvider } from '@/providers/counter-store-provider' import { HomePage } from '@/components/pages/home-page' export default function Home() { return ( <CounterStoreProvider> <HomePage /> </CounterStoreProvider> ) } Edit this page Previous SSR and Hydration Next Immer middleware --- ## Page: https://zustand.docs.pmnd.rs/apis/create-with-equality-fn # createWithEqualityFn ⚛️ How to create efficient stores `createWithEqualityFn` lets you create a React Hook with API utilities attached, just like `create`. However, it offers a way to define a custom equality check. This allows for more granular control over when components re-render, improving performance and responsiveness. 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`. const useSomeStore = createWithEqualityFn(stateCreatorFn, equalityFn) * Types * Signature * Reference * Usage * Updating state based on previous state * Updating Primitives in State * Updating Objects in State * Updating Arrays in State * Updating state with no store actions * Subscribing to state updates * Troubleshooting * I’ve updated the state, but the screen doesn’t update ## Types ### Signature createWithEqualityFn<T>()(stateCreatorFn: StateCreator<T, [], []>, equalityFn?: (a: T, b: T) => boolean): UseBoundStore<StoreApi<T>> ## 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. This example shows how you can support **updater functions** within **actions**. 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<AgeStore>()( (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 ( <> <h1>Your age: {age}</h1> <button type="button" onClick={() => { increment() increment() increment() }} > +3 </button> <button type="button" onClick={() => { increment() }} > +1 </button> </> ) } ### 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` import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type XStore = number const useXStore = createWithEqualityFn<XStore>()(() => 0, shallow) export default function MovingDot() { const x = useXStore() const setX = (nextX: number) => { useXStore.setState(nextX, true) } const position = { y: 0, x } return ( <div onPointerMove={(e) => { setX(e.clientX) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } ### 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. 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<PositionStore>()( (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 ( <div onPointerMove={(e) => { setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } ### 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(...)`. import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' type PositionStore = [number, number] const usePositionStore = createWithEqualityFn<PositionStore>()( () => [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 ( <div onPointerMove={(e) => { setPosition([e.clientX, e.clientY]) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } ### 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). 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 ( <div style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} onMouseEnter={(event) => { 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), }) }} /> </div> ) } ### 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. 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<PositionStore>()( (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 ( <div style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} onMouseEnter={(event) => { 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), }) }} /> </div> ) } ## 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: 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<PersonStore>()( (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<HTMLInputElement>) { person.firstName = e.target.value } function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) { person.lastName = e.target.value } function handleEmailChange(e: ChangeEvent<HTMLInputElement>) { person.email = e.target.value } return ( <> <label style={{ display: 'block' }}> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label style={{ display: 'block' }}> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label style={{ display: 'block' }}> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName} {person.lastName} ({person.email}) </p> </> ) } For example, this line mutates the state from a past render: 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: 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! 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<PersonStore>()( (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<HTMLInputElement>) { setPerson({ ...person, firstName: e.target.value }) } function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) { setPerson({ ...person, lastName: e.target.value }) } function handleEmailChange(e: ChangeEvent<HTMLInputElement>) { setPerson({ ...person, email: e.target.value }) } return ( <> <label style={{ display: 'block' }}> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label style={{ display: 'block' }}> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label style={{ display: 'block' }}> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName} {person.lastName} ({person.email}) </p> </> ) } Edit this page Previous createStore Next create ⚛️ --- ## Page: https://zustand.docs.pmnd.rs/apis/create # create ⚛️ How to create stores `create` lets you create a React Hook with API utilities attached. const useSomeStore = create(stateCreatorFn) * Types * Signature * Reference * Usage * Updating state based on previous state * Updating Primitives in State * Updating Objects in State * Updating Arrays in State * Updating state with no store actions * Subscribing to state updates * Troubleshooting * I’ve updated the state, but the screen doesn’t update ## Types ### Signature create<T>()(stateCreatorFn: StateCreator<T, [], []>): UseBoundStore<StoreApi<T>> ## 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. This example shows how you can support **updater functions** within **actions**. 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<AgeStore>()((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 ( <> <h1>Your age: {age}</h1> <button onClick={() => { increment() increment() increment() }} > +3 </button> <button onClick={() => { increment() }} > +1 </button> </> ) } ### 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` import { create } from 'zustand' type XStore = number const useXStore = create<XStore>()(() => 0) export default function MovingDot() { const x = useXStore() const setX = (nextX: number) => { useXStore.setState(nextX, true) } const position = { y: 0, x } return ( <div onPointerMove={(e) => { setX(e.clientX) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } ### 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. 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<PositionStore>()((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 ( <div onPointerMove={(e) => { setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } ### 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(...)`. import { create } from 'zustand' type PositionStore = [number, number] const usePositionStore = create<PositionStore>()(() => [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 ( <div onPointerMove={(e) => { setPosition([e.clientX, e.clientY]) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } ### 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). 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 ( <div style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} onMouseEnter={(event) => { 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), }) }} /> </div> ) } ### 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. 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<PositionStore>()((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 ( <div style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} onMouseEnter={(event) => { 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), }) }} /> </div> ) } ## 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: import { create } from 'zustand' type PersonStoreState = { firstName: string lastName: string email: string } type PersonStoreActions = { setPerson: (nextPerson: Partial<PersonStoreState>) => void } type PersonStore = PersonStoreState & PersonStoreActions const usePersonStore = create<PersonStore>()((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<HTMLInputElement>) { person.firstName = e.target.value } function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) { person.lastName = e.target.value } function handleEmailChange(e: ChangeEvent<HTMLInputElement>) { person.email = e.target.value } return ( <> <label style={{ display: 'block' }}> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label style={{ display: 'block' }}> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label style={{ display: 'block' }}> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName} {person.lastName} ({person.email}) </p> </> ) } For example, this line mutates the state from a past render: 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: 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! 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<PersonStore>()((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<HTMLInputElement>) { setPerson({ ...person, firstName: e.target.value }) } function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) { setPerson({ ...person, lastName: e.target.value }) } function handleEmailChange(e: ChangeEvent<HTMLInputElement>) { setPerson({ ...person, email: e.target.value }) } return ( <> <label style={{ display: 'block' }}> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label style={{ display: 'block' }}> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label style={{ display: 'block' }}> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName} {person.lastName} ({person.email}) </p> </> ) } Edit this page Previous createWithEqualityFn ⚛️ Next shallow --- ## Page: https://zustand.docs.pmnd.rs/apis/shallow # shallow How compare simple data effectively `shallow` lets you run fast checks on simple data structures. It effectively identifies changes in **top-level** properties when you're working with data structures that don't have nested objects or arrays within them. Note Shallow lets you perform quick comparisons, but keep its limitations in mind. const equal = shallow(a, b) * Types * Signature * Reference * Usage * Comparing Primitives * Comparing Objects * Comparing Sets * Comparing Maps * Troubleshooting * Comparing objects returns `false` even if they are identical. * Comparing objects with different prototypes ## Types ### Signature shallow<T>(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. 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. 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. 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. 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`. 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: 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: 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.` const a = Object.create({}) // -> prototype is `{}` const b = {} // -> prototype is `Object.prototype` shallow(a, b) // -> false Edit this page Previous create ⚛️ Next useShallow ⚛️ --- ## Page: https://zustand.docs.pmnd.rs/hooks/use-store-with-equality-fn # useStoreWithEqualityFn ⚛️ How to use vanilla stores effectively in React `useStoreWithEqualityFn` is a React Hook that lets you use a vanilla store in React, just like `useStore`. However, it offers a way to define a custom equality check. This allows for more granular control over when components re-render, improving performance and responsiveness. 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`. const someState = useStoreWithEqualityFn(store, selectorFn, equalityFn) * Types * Signature * Reference * Usage * Use a vanilla store in React * Using dynamic vanilla stores in React * Using scoped (non-global) vanilla store in React * Using dynamic scoped (non-global) vanilla stores in React * Troubleshooting ### Signature useStoreWithEqualityFn<T, U = T>(store: StoreApi<T>, 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. 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<PositionStore>()((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. function MovingDot() { const position = useStoreWithEqualityFn( positionStore, (state) => state.position, shallow, ) const setPosition = useStoreWithEqualityFn( positionStore, (state) => state.setPosition, shallow, ) return ( <div onPointerMove={(e) => { setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } Finally, we’ll render the `MovingDot` component in our `App` component. export default function App() { return <MovingDot /> } Here is what the code should look like: 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<PositionStore>()((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 ( <div onPointerMove={(e) => { setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } export default function App() { return <MovingDot /> } ### 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. import { createStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore<CounterStore>()((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. const defaultCounterStores = new Map< string, ReturnType<typeof createCounterStore> >() 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. const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStoreWithEqualityFn( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), (state) => state, shallow, ) return ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. export default function App() { return <Tabs /> } Here is what the code should look like: 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<CounterStore>()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const defaultCounterStores = new Map< string, ReturnType<typeof createCounterStore> >() 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 ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) } ### 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. type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore<PositionStore>()((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. const PositionStoreContext = createContext<ReturnType< typeof createPositionStore > | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [positionStore] = useState(createPositionStore) return ( <PositionStoreContext.Provider value={positionStore}> {children} </PositionStoreContext.Provider> ) } 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. function usePositionStore<U>(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. function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return ( <div onPointerMove={(e) => { setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: color, borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` components, each with its own independent state. export default function App() { return ( <div style={{ display: 'flex' }}> <PositionStoreProvider> <MovingDot color="red" /> </PositionStoreProvider> <PositionStoreProvider> <MovingDot color="blue" /> </PositionStoreProvider> </div> ) } Here is what the code should look like: 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<PositionStore>()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } const PositionStoreContext = createContext<ReturnType< typeof createPositionStore > | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [positionStore] = useState(createPositionStore) return ( <PositionStoreContext.Provider value={positionStore}> {children} </PositionStoreContext.Provider> ) } function usePositionStore<U>(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 ( <div onPointerMove={(e) => { setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: color, borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } export default function App() { return ( <div style={{ display: 'flex' }}> <PositionStoreProvider> <MovingDot color="red" /> </PositionStoreProvider> <PositionStoreProvider> <MovingDot color="blue" /> </PositionStoreProvider> </div> ) } ### 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. type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore<CounterStore>()((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. const createCounterStoreFactory = ( counterStores: Map<string, ReturnType<typeof createCounterStore>>, ) => { 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. const CounterStoresContext = createContext(null) const CounterStoresProvider = ({ children }) => { const [stores] = useState( () => new Map<string, ReturnType<typeof createCounterStore>>(), ) return ( <CounterStoresContext.Provider value={stores}> {children} </CounterStoresContext.Provider> ) } Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a given tab. const useCounterStore = <U,>( 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. function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) } Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. export default function App() { return ( <CounterStoresProvider> <Tabs /> </CounterStoresProvider> ) } Here is what the code should look like: 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<CounterStore>()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const createCounterStoreFactory = ( counterStores: Map<string, ReturnType<typeof createCounterStore>>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const CounterStoresContext = createContext<Map< string, ReturnType<typeof createCounterStore> > | null>(null) const CounterStoresProvider = ({ children }: { children: ReactNode }) => { const [stores] = useState( () => new Map<string, ReturnType<typeof createCounterStore>>(), ) return ( <CounterStoresContext.Provider value={stores}> {children} </CounterStoresContext.Provider> ) } const useCounterStore = <U,>( 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 ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) } export default function App() { return ( <CounterStoresProvider> <Tabs /> </CounterStoresProvider> ) } ## Troubleshooting TBD Edit this page Previous useShallow ⚛️ Next useStore ⚛️ --- ## Page: https://zustand.docs.pmnd.rs/hooks/use-store # useStore ⚛️ How to use vanilla stores in React `useStore` is a React Hook that lets you use a vanilla store in React. const someState = useStore(store, selectorFn) * Types * Signature * Reference * Usage * Use a vanilla store in React * Using dynamic vanilla stores in React * Using scoped (non-global) vanilla store in React * Using dynamic scoped (non-global) vanilla stores in React * Troubleshooting ## Types ### Signature useStore<StoreApi<T>, U = T>(store: StoreApi<T>, selectorFn?: (state: T) => U) => UseBoundStore<StoreApi<T>> ## 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. type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const positionStore = createStore<PositionStore>()((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. function MovingDot() { const position = useStore(positionStore, (state) => state.position) const setPosition = useStore(positionStore, (state) => state.setPosition) return ( <div onPointerMove={(e) => { setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } Finally, we’ll render the `MovingDot` component in our `App` component. export default function App() { return <MovingDot /> } Here is what the code should look like: 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<PositionStore>()((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 ( <div onPointerMove={(e) => { setPosition({ x: e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '100vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } export default function App() { return <MovingDot /> } ### 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. type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore<CounterStore>()((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. const defaultCounterStores = new Map< string, ReturnType<typeof createCounterStore> >() 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. const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useStore( getOrCreateCounterStoreByKey(`tab-${currentTabIndex}`), ) return ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. export default function App() { return <Tabs /> } Here is what the code should look like: 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<CounterStore>()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const defaultCounterStores = new Map< string, ReturnType<typeof createCounterStore> >() 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 ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) } ### 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. type PositionStoreState = { position: { x: number; y: number } } type PositionStoreActions = { setPosition: (nextPosition: PositionStoreState['position']) => void } type PositionStore = PositionStoreState & PositionStoreActions const createPositionStore = () => { return createStore<PositionStore>()((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. const PositionStoreContext = createContext<ReturnType< typeof createPositionStore > | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [positionStore] = useState(createPositionStore) return ( <PositionStoreContext.Provider value={positionStore}> {children} </PositionStoreContext.Provider> ) } 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. function usePositionStore<U>(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. function MovingDot({ color }: { color: string }) { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return ( <div onPointerMove={(e) => { setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: color, borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } Finally, we'll bring everything together in the `App` component, where we render two `MovingDot` components, each with its own independent state. export default function App() { return ( <div style={{ display: 'flex' }}> <PositionStoreProvider> <MovingDot color="red" /> </PositionStoreProvider> <PositionStoreProvider> <MovingDot color="blue" /> </PositionStoreProvider> </div> ) } Here is what the code should look like: 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<PositionStore>()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) } const PositionStoreContext = createContext<ReturnType< typeof createPositionStore > | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [positionStore] = useState(createPositionStore) return ( <PositionStoreContext.Provider value={positionStore}> {children} </PositionStoreContext.Provider> ) } function usePositionStore<U>(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 ( <div onPointerMove={(e) => { setPosition({ x: e.clientX > e.currentTarget.clientWidth ? e.clientX - e.currentTarget.clientWidth : e.clientX, y: e.clientY, }) }} style={{ position: 'relative', width: '50vw', height: '100vh', }} > <div style={{ position: 'absolute', backgroundColor: color, borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) } export default function App() { return ( <div style={{ display: 'flex' }}> <PositionStoreProvider> <MovingDot color="red" /> </PositionStoreProvider> <PositionStoreProvider> <MovingDot color="blue" /> </PositionStoreProvider> </div> ) } ### 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. import { createStore } from 'zustand' type CounterState = { count: number } type CounterActions = { increment: () => void } type CounterStore = CounterState & CounterActions const createCounterStore = () => { return createStore<CounterStore>()((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. const createCounterStoreFactory = ( counterStores: Map<string, ReturnType<typeof createCounterStore>>, ) => { 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. const CounterStoresContext = createContext(null) const CounterStoresProvider = ({ children }) => { const [stores] = useState( () => new Map<string, ReturnType<typeof createCounterStore>>(), ) return ( <CounterStoresContext.Provider value={stores}> {children} </CounterStoresContext.Provider> ) } Now, we’ll create a custom hook, `useCounterStore`, that lets us access the correct store for a given tab. const useCounterStore = <U>( 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. function Tabs() { const [currentTabIndex, setCurrentTabIndex] = useState(0) const counterState = useCounterStore( `tab-${currentTabIndex}`, (state) => state, ) return ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) } Finally, we'll create the `App` component, which renders the tabs and their respective counters. The counter state is managed independently for each tab. export default function App() { return ( <CounterStoresProvider> <Tabs /> </CounterStoresProvider> ) } Here is what the code should look like: 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<CounterStore>()((set) => ({ count: 0, increment: () => { set((state) => ({ count: state.count + 1 })) }, })) } const createCounterStoreFactory = ( counterStores: Map<string, ReturnType<typeof createCounterStore>>, ) => { return (counterStoreKey: string) => { if (!counterStores.has(counterStoreKey)) { counterStores.set(counterStoreKey, createCounterStore()) } return counterStores.get(counterStoreKey)! } } const CounterStoresContext = createContext<Map< string, ReturnType<typeof createCounterStore> > | null>(null) const CounterStoresProvider = ({ children }: { children: ReactNode }) => { const [stores] = useState( () => new Map<string, ReturnType<typeof createCounterStore>>(), ) return ( <CounterStoresContext.Provider value={stores}> {children} </CounterStoresContext.Provider> ) } const useCounterStore = <U,>( 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 ( <div style={{ fontFamily: 'monospace' }}> <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '1px solid salmon', paddingBottom: 4, }} > <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(0)} > Tab 1 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(1)} > Tab 2 </button> <button type="button" style={{ border: '1px solid salmon', backgroundColor: '#fff', cursor: 'pointer', }} onClick={() => setCurrentTabIndex(2)} > Tab 3 </button> </div> <div style={{ padding: 4 }}> Content of Tab {currentTabIndex + 1} <br /> <br /> <button type="button" onClick={() => counterState.increment()}> Count: {counterState.count} </button> </div> </div> ) } export default function App() { return ( <CounterStoresProvider> <Tabs /> </CounterStoresProvider> ) } ## Troubleshooting TBD Edit this page Previous useStoreWithEqualityFn ⚛️ Next combine