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.
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.
I can think of many reasons you should use Zustand. Here are a few top ones to consider:
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.
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.
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.
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!
Zustand has a few notable features that are key for its adoption amongst developers.
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.
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 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.
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.
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.
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 }, }));
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.
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:
useShallow()
Hook to prevent rerendering components when specific properties within your state are modifiedRead LogRocket’s Guide to requirements management software article to learn how to plan and decide what software to use for your upcoming projects.
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. |
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
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.
Sign up nowEfficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.