Since the rise of dynamic applications, state management has been a primary concern for developers using modern frontend frameworks to build SPAs. State management solutions enable developers to share data locally in a component and globally between the multiple pages of an application.
This guide will teach you about Zustand, a fast-rising state management tool within the React community. You will learn about its simplistic approach to managing state, and how it compares to existing tools such as Mobx and Redux.
By the end of this guide, you will have practical working experience with Zustand as you follow through a demo process of adopting Zustand into the existing React tic-tac-toe game. Let’s get started.
What is Zustand?
Zustand (a German word, pronounced zush-tand) was created by Jürgen Martens in 2019 as an alternative to the popular Redux tool. Developers found Redux difficult to learn and work with due to its complexity, architecture, and boilerplate-heavy setup.
At 1.2KB, Zustand is a lightweight state solution for React applications. Although lightweight, Zustand scales to manage states within large applications with hundreds to thousands of components.
At the basic level, a Zustand store is a React Hook with an object containing different values that represent the state. Developers call the Hook to access its values from the state object directly within functional components without needing a provider wrapper around the component tree.
Modifying state values with Zustand is possible through the set()
function. This function merges values in an immutable pattern.
Zustand promotes the Flux “single source of truth” principle of relying directly on the centralized store for data to avoid the zombie child rendering problem. The zombie child problem results in data inconsistencies, as the parent components re-render on state change while the child components reference the old store values.
Further reading:
- Managing React state with Zustand
- How to access previous props or state with React Hooks #Using Zustand for state management
Why choose Zustand?
I can think of many reasons you should use Zustand. Here are a few top ones to consider:
- Performance & bundle size — Zustand provides various performance-oriented features, making it an excellent choice for developers with performance-critical apps. The Zustand core package is also only a few kilobytes large, leading to a minimal increase on the application’s overall bundle size. The default use of shallow comparison and the option to selective state subscription contributes to reducing frequent component re-renderings
- Developer experience (DX) — Zustand offers an amazing DX. Its straightforward API makes it simple to work with, it requires minimal boilerplate code to set up, and it’s intuitive to use for developers who enjoy functional programming. If you’re familiar with React, you’ll enjoy using Zustand as it aligns with React’s core principles such as immutable state and unidirectional data flow principles. Zustand also provides features that help reduce the need to duplicate code within a codebase, like the ability to auto-generate selectors to access state properties
- Community — Considering Zustand’s primary target is the React library, which is being used by thousands of SPAs on the web, it’s being widely adopted by frontend developers. Zustand now has a fast-growing developer community with over 44.7K stars on GitHub, 3.5M weekly downloads, and discussion threads across the React communities on Reactiflux and Reddit
- Integrations — Zustand has a decent-sized ecosystem, offering official and third-party integrations built by the community to add functionalities not provided in the core Zustand API. Offline state persistence is one such functionality enabling developers to persist state for offline-first applications
- Documentation — Zustand has a comprehensive and detailed documentation site that provides step-by-step guides on integrating it into your React application. The guides go from providing instructions with few lines of code to advanced steps and highlight various recommendations for production-ready applications. The documentation provides a comparison page detailing how Zustand compares and shares similarities with Redux, Jotai, Recoil, and Valtio
Zustand’s GitHub repository also provides a demo project for you to clone, install its dependencies, and get a quick idea of what working with Zustand involves.
Getting started with Zustand
Now that you better understand how Zustand improves the state management of React applications, let’s adopt it in an existing tic-tac-toe game built with React. You will install Zustand, set up a game store, and bind it to the components within the /game
page.
Setting up the application
Launch your terminal application and execute the command below to clone or manually download the React application from the GitHub repository:
git clone https://github.com/vickywane/React-tic-tac-toe.git
Change your terminal directory into the cloned React-tic-tac-toe
project folder:
cd React-tic-tac-toe
Within the next section, you will install the Zustand npm package into the React-tic-tac-toe
project.
Setting up Zustand
Unlike many other state management solutions, Zustand is contained in a single, small-sized dependency — part of what makes it so lightweight, with few setup requirements.
Execute the following command to install Zustand into the React-tic-tac-toe
project:
yarn add zustand
Next, we’ll create a store holding values for the tic-tac-toe game state to track when a user plays in a tile within the boxes, wins, loses, or draws in a match.
Create a gameStore.js
file in the /src/state
directory to create the game store Hook. Let’s gradually put the code in the file across multiple code steps:
// ./src/state/gameStore.js
import { create } from "zustand";
import { findUniqueRandomNumber, getWinner } from "../utils";
const initalState = {
userGameRecord: {
wins: [],
losses: [],
},
matchedTiles: [],
isGameDisabled: false,
currentWinner: undefined,
gameStatus: "ONGOING",
gameTiles: new Array(9).fill(null),
currentPlayer: null,
gameView: "IN-GAME-VIEW",
};
Next, create and export the store Hook from the /src/state/gameStore.js
file. You will use the create()
method to create the store Hook object with state properties and modifier methods:
// ./src/state/gameStore.js
export const useGameStore = create((set) =
> ({
...initalState,
resetGameState: () => set(initalState),
setCurrentPlayer: (player) => set({ currentPlayer: player }),
changeGameStatus: (status) => set({ gameStatus: status }),
setGameRecord: (record) => set({ userGameRecord: record }),
setGameTiles: (tiles) => set({ gameTiles: tiles }),
disableGame: (status) => set({ isGameDisabled: status }),
setWinner: (winner) => set({ currentWinner: winner }),
setMatchedTiles: (tiles) => set({ matchedTiles: tiles }),
changeGameView: (view) => set({ gameView: view }),
}));
The state Hook created with the code above merges in the initalState
values using the spread operator and has various methods to modify the state values. One of the methods of concern is resetGameState()
, which resets the entire state to its initial values.
Next, let’s create the two last methods, which will be called from the components when a user clicks a tile:
// ./src/state/gameStore.js
handleTileClick: (position, state) =
> {
const {
gameTiles,
isGameDisabled,
setWinner,
setGameTiles,
processGameRecord,
changeGameStatus,
disableGame,
setCurrentPlayer,
setMatchedTiles,
} = useGameStore.getState();
if (!gameTiles[position] && !isGameDisabled) {
let tilesCopy = [...gameTiles];
if (!tilesCopy.includes(null)) {
changeGameStatus("TIE");
disableGame(true);
return;
}
tilesCopy[position] = state.player;
const opponentPlayer = state.player === "X" ? "O" : "X";
setCurrentPlayer(opponentPlayer);
setTimeout(() => {
tilesCopy[findUniqueRandomNumber(tilesCopy)] = opponentPlayer;
const gameResult = getWinner(tilesCopy);
if (gameResult?.winningPlayer) {
disableGame(true);
setMatchedTiles(gameResult?.matchingTiles);
setWinner(gameResult?.winningPlayer);
changeGameStatus("WIN");
processGameRecord({
wins: gameResult?.winningPlayer,
loss: gameResult?.winningPlayer,
});
}
setCurrentPlayer(state.player);
setGameTiles(tilesCopy);
}, 500);
} else if (!gameTiles.includes(null)) {
changeGameStatus("TIE");
disableGame(true);
}
},
processGameRecord: ({ wins, loss }) => {
const { userGameRecord, setGameRecord } = useGameStore.getState();
let gameLosses = [...userGameRecord.losses];
let gameWins = [...userGameRecord.wins];
if (wins) {
gameWins.push(wins);
}
if (loss) {
gameLosses.push(loss);
}
setGameRecord({
wins: gameWins,
losses: gameLosses,
});
},
Replace the useState
Hooks and functions within the Game
component in the ./src/pages/game.js
file with the Zustand state properties as shown below:
const {
handleTileClick,
gameTiles,
isGameDisabled,
currentPlayer,
matchedTiles,
resetGameState,
currentWinner,
userGameRecord,
gameStatus,
gameView,
changeGameView,
setCurrentPlayer,
} = useGameStore((state) =
> state);
With that, managing the state of the React-tic-tac-toe
application has moved over to Zustand!
Key Zustand features to know
Zustand has a few notable features that are key for its adoption amongst developers.
Modifying state
Zustand follows the immutability concept used with React’s useState
Hooks for the local component state to manage state updates efficiently.
The state properties in a Zustand store are updated by merging new states using the set()
function. Treating the Zustand state as immutable makes it easier to reset the state properties to the initial values.
To update more complex state objects with nested levels, you need to pass a callback to the set()
function with the spread operator to merge the old state with the new ones explicitly.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to use the React children prop with TypeScript
- Explore creating a custom mouse cursor with CSS
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
The following code block demonstrates how to explicitly update a nested object in a Zustand store with a combination of the set()
function and spread operator:
const initialState = {
gameRecord: {
losses: [],
metadata: {
totalSession: null,
totalMoves: 0,
},
},
};
export const useAGameStore = create((set) =
> ({
...initialState,
setMetadata: (state, metadata) =>
set({
gameRecord: {
...state.gameRecord,
metadata: metadata,
},
}),
}));
Selectors
Selectors are functions for extracting values from the Zustand. Zustand allows developers to pass a callback to the useStore()
Hook to extract a specific state from the store.
Zustand enables you to do more than retrieve state values with selectors. It allows you to derive or compute values based on the existing state in your component.
In the React-tic-tac-toe
game, a derived or computed value would let us determine the current winner of the game based on the play history, as shown in the following code:
const useGameStats = () =
>
useGameStore((state) => ({
leadingWinner:
state.userGameRecord.wins.length > state.userGameRecord.losses.length
? "Player"
: "Computer",
}));
The computed leadingWinner
property returned from the useGameStats()
function above returns a string value to indicate if the player or computer is winning the game.
In addition to computed values, Zustand provides an auto-generated selectors feature to reduce writing callbacks to access state values.
Preventing rerenders
Though the beauty of React lies in it being reactive, developers also need to prevent components from rerendering often. Frequent rerendering will result in lags or stutters in the component elements as render cycles require computations to update the DOM.
Zustand provides the useShallow()
Hook for developers to use when they need to prevent rerendering the entire component due to a state change. The useShallow()
Hook optimizes state updates through the use of shallow comparison to compare top-level properties.
SSR/hydration
Frontend frameworks and libraries that offer server-side rendering (SSR) support can improve both application performance and UX, as SSR reduces the JavaScript load and execution time.
Zustand has well-documented support for applications using SSR with the Next.js framework. The catch is that this well-documented support is necessary because using SSR with Next.js and Zustand is complex due to its design.
Some potential issues that could arise include the application being rendered twice — on the server side and client side, often resulting in hydration errors — and the need to initialize the Zustand at the component level using React Context.
To resolve these challenges, Zustand recommends creating the store per request for the initializations and not using a global store or accessing the stores from server components.
Props
As you build your application, you may need to initialize a store with data retrieved within the component such as authentication or user data to perform dependency injection.
Zustand supports initializing a store with data passed from a client component, and then using the createContext
provider from React to wrap the component if needed. To do this, you need to use an initialization function taking parameters and returning the create()
Hook.
The following code demonstrates the initialization function for creating a game store that accepts a user object as a prop and returns the store:
const createGameStore = (user) =
> create((set) => ({
data: {
user: user || null
},
}));
Testing options
Zustand supports the Jest and Vitest test runners to mock and test your store. If your store makes network requests, Mock Service Worker (MSW) is an efficient library to intercept the network requests and return data for your assertions.
Zustand recommends using the React Testing Library (RTL) to test the components consuming your Zustand store. If you are using Zustand within a React Native application, the React Native Testing Library (RNTL) is also an excellent choice for testing your components.
Use cases for Zustand
Since its release, Zustand has proven to be an excellent choice for developers who want to incorporate state management into their apps. Here are two scenarios where Zustand will be the best solution to manage your application state:
- Performance critical applications — Being lightweight, Zustand is a great fit for large performance-critical applications such as real-time monitoring systems, games, and collaborative and interactive applications with frequent state updates. Zustand’s slices pattern enables you to split the state of such large applications into individual stores to achieve modularity. Zustand also provides the
useShallow()
Hook to prevent rerendering components when specific properties within your state are modified - MVP applications — Zustand’s lower learning curve, excellent DX, and minimal setup requirements make it the best fit for developers building MVP applications. With a single package installation and little file changes in a short engineering time, you will have your Zustand store set up
Read LogRocket’s Guide to requirements management software article to learn how to plan and decide what software to use for your upcoming projects.
Zustand vs. similar
Let’s see how Zustand compares with Redux and MobX. These two older React state management libraries existed before Zustand was developed, which give them certain advantages over a newer solution like Zustand. However, Zustand still has plenty of pros that make it worth considering:
Feature | Zustand | Redux | MobX |
---|---|---|---|
Features | Limited. Provides only the core state management API to keep the bundle size thin. Offers middleware and a fast-growing ecosystem with official and third-party additional packages. | Feature-rich. Provides features for state management, middleware support for asynchronous actions, logging, routing, and devtools integration for debugging. | Feature-rich. MobX provides features for reactive state management, dependency tracking, decorators, computed values, transactions, fine-grained reactions, and DevTools for debugging. |
Performance | Fastest. It provides incredible performance due to its simple core API, direct store access design, selective state subscription, and shallow comparison features. | Good performance. Larger dependency bundle size and complex setup often introduce a performance overhead when poorly managed. | Great performance for reactive apps and requires a lesser boilerplate, and complexity unlike Redux, but a larger bundle size, unlike Zustand. |
Community | Fast-growing communities on GitHub, Discord, and Stack Overflow. | A large and active community of mostly React developers. | Large and active; mostly JavaScript developers using Object Oriented Programming (OOP) paradigms. |
Documentation | Detailed documentation covering all basic features of Zustand. | Extensive official documentation and a plethora of technical articles, books, and video tutorials from developers within the community. | Comprehensive. |
Learning Curve | Easier. Developers familiar with React and JavaScript find Zustand easier to work with due to its alignment with React principles. | Steep learning curve. | Moderate for developers familiar with OOP. |
Further reading:
- MobX adoption guide
- Understanding Redux: A tutorial with examples
- A guide to choosing the right React state management solution
Conclusion
Congratulations on completing this adoption guide on Zustand, the open source state management solution developed as a straightforward alternative to Redux!
In this guide, we explored Zustand’s core functions to create a store, modify its state properties, and consume it from a component. Then we considered Zustand’s performance, DX, community, and ecosystem to understand their values.
Finally, the table above provides a helpful comparison between Zustand, Redux, and MobX to help you determine which state management solution is right for your needs.