Website logo

08 Feb 2024

How
To
Create
Global
State
With
Zustand

Say we're currently handling a website project with a white interface and bright accent colors. However, at some point, the client requests that the website supports a Dark Theme feature. When visitors click a button in the menu, the website's colors, initially bright, shift towards darker tones for a more eye-friendly experience.

import { useState } from 'react';

const Menu = () => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(theme === 'dark' ? 'light' : 'dark');
  };

  return (
    <div>
      <h1>Menu</h1>

      <button
        style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }}
        type="button"
        onClick={toggleTheme}
      >
        Toggle Theme
      </button>
    </div>
  );
};

export default Menu;

In the implementation of the above component, we can see that when the "Toggle Theme" button is clicked, the button's color changes based on the value of the "theme" state. However, there's an issue here: the "theme" state can only be accessed by the Menu component itself, whereas the value of the "theme" state needs to be accessed by other components.

In essence, states have two types: local state and global state. Local state is a state applied within a component and can be accessed by that component as well as its child components through props. On the other hand, global state is a state accessible by all components.

In this case, we need to change the "theme" state from local state to global state using the help of a state management library called Zustand.

What is Zustand

Zustand is a state management library for React that we can use to store state globally. Zustand is easier to use and configure compared to other state management libraries like Redux and has quite good documentation.

Additionally, Zustand has some useful middleware, such as the persist middleware that we can use to synchronize Zustand state with storage like LocalStorage and others. In my opinion, Zustand is also more comfortable to use than the Context API. Due to its simplicity, I often use it in my projects.

How to Implement Zustand

First, we need to install Zustand through npm in our React project.

npm install zustand

Then create a slice file to store our "theme" state in the store/slices/theme.slice.ts directory. A slice is a part of the store that usually contains the state we create along with functions used to change the value of that state. Here, we create Light Theme as the default theme.

import { StateCreator } from 'zustand';

type Theme = 'dark' | 'light';

export type ThemeSlice = {
  theme: Theme;
  setTheme: (value: Theme) => void;
};

export const createThemeSlice: StateCreator<
  ThemeSlice,
  [],
  [],
  ThemeSlice
> = (set) => ({
  theme: 'light',
  setTheme: (theme) => set({ theme }),
});

Next, we also need to add the slice we created to the store file in the store/store.ts directory.

import { create } from 'zustand';

import { ThemeSlice, createThemeSlice } from './slices/theme.slice';

export const useStore = create<ThemeSlice>()(
  (...a) => ({
    ...createThemeSlice(...a),
  })
);

After that, we can use the global state we created with Zustand.

// Import the store we created earlier.
import { useStore } from "../../store"; 

const Menu = () => {
  const theme = useStore((state) => state.theme);
  const setTheme = useStore((state) => state.setTheme);

  const toggleTheme = () => {
    setTheme(theme === 'dark' ? 'light' : 'dark');
  };

  return (
    <div>
      <h1>Menu</h1>

      <button
        style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }}
        type="button"
        onClick={toggleTheme}
      >
        Toggle Theme
      </button>
    </div>
  );
};

export default Menu;

You can also apply it to other components. With this, the "theme" state, which was originally a local state, now becomes a global state.

import { useStore } from "../../store"; 

const Header = () => {
  const theme = useStore((state) => state.theme);

  return (
    <div>
      <h1
        style={{
          backgroundColor: theme === 'dark' ? 'white' : 'black'
        }}
      >
        My Website
      </h1>
    </div>
  );
};

export default Header;

Implementation of Persistent State

Global state works well, but when we refresh the website in the browser, the state goes back to its initial value, which is "light," making the Light Theme active again. Ideally, when a website visitor chooses the Dark Theme and refreshes the website, the Dark Theme should automatically be active. Here, we can make Zustand save its state in the browser's LocalStorage, so when the website is refreshed, Zustand checks its stored state in LocalStorage and retrieves it to place it in the store. We just need to use the persist middleware on the store we created earlier.

import { create } from "zustand";
import { persist } from "zustand/middleware";

import { ThemeSlice, createThemeSlice } from "./slices/theme.slice";

export const useStore = create<ThemeSlice>()(
  persist(
    (...a) => ({
      ...createThemeSlice(...a),
    }),
    {
      name: "main-storage",
    }
  )
);

By default, the persist middleware uses LocalStorage as its storage, but we can create a persist middleware to use a different storage like SessionStorage if desired.

Conclusion

By using Zustand, the implementation of global state becomes much easier compared to using Redux. Moreover, Zustand has many features that we can use to support the development of a project.