zustand

The client global store library: https://github.com/pmndrs/zustand

We use Zustand as a global store for sharing data between components, both in an outside of React. This serves the same purpose as things like Redux, for having reactive global state that lets components communicate outside of direct props and callbacks.

Stores

To declare a store, you call create with a store type, which returns a hook that lets you access the store data. The hook also includes methods for getting and updating data from the store outside of react.

Exporting helpers

The syntax for getting a value out of a store can be a little verbose, since you have to pass a selector function to the store hook. Instead, it is good practice to not export the store itself from a file, and instead export specific hooks data out of the store.

Also, Zustand lets you either declare setters as methods actions in the store, or exported functions. Their docs say that either is fine, and in practice exported functions seem easier to work with, as they don't require an extra hook to generate them, and reduce duplication in the store interface.

Finally, for shared logic for returning a calculated value of the store, exporting a selector wrapped in a hook makes writing components, and sharing logic between them, easier.

However in tests, it is sometimes useful to have direct access to the store to set up test cases. So you can export the store itself, but leave a doc comment reminding devs not to directly import the store in implementation code.

Instead of

// name.store.ts
import create from "zustand";

interface NameStore {
  firstName: string;
  lastName: string;
  setName: (name: string) => void;
}

export const useName = create<NameStore>((set) => ({
  firstName: "Alice",
  lastName: "Turing",
  setName: (name) => {
    const [firstName, lastName] = name.split(" ");
    set({ firstName, lastName });
  },
}));

// MyName.tsx
import React from "react";
import { useName } from "./name.store";

export const MyName = () => {
  const name = useName((state) => `${state.firstName} ${state.lastName}`);
  const setName = useName((state) => state.setName);

  return (
    <input value={name} onChange={(event) => setName(event.target.value)} />
  );
};

you can instead export helpers, which both make components easier to write, and make it clear which parts of the store are publicly available

// name.store.ts
import create from "zustand";

interface NameStore {
  firstName: string;
  lastName: string;
}

/**
 * Name Store.
 * @test only import directly in tests.
 */
export const useNameStore = create<NameStore>(() => ({
  firstName: "Alice",
  lastName: "Turing",
}));

export const useName = () => {
  return useNameStore((state) => `${state.firstName} ${state.lastName}`);
};

export const setName = (name: string) => {
  const [firstName, lastName] = name.split(" ");
  useNameStore.setState({ firstName, lastName });
};

// MyName.tsx
import { useName, setName } from "./name.store";

export const MyName = () => {
  const name = useName();

  return (
    <input value={name} onChange={(event) => setName(event.target.value)} />
  );
};

simpleStore

If you have a single piece of state you are storing with a useState, and either want to share it between multiple components, or cache its value as components are created and destroyed, simpleStore (packages/shared/store/simpleStore.ts) may be helpful.

To use it, replace a local useState

const MyComponent = () => {
   const [count, setCount] = useState();

with a global simpleStore

const useCount = simpleStore(0);

const MyComponent = () => {
   const [count, setCount] = useState();

Now, the count state will persist after MyComponent is destroyed, and can be shared with multiple components.

Last updated