reactjsreact-contextzustand

Can I use a Zustand store inside a React context to create multiple instances of the store?


I'm trying to create a post component where each post has its own state. The post component is really complex, so it's necessary to use the React context hook.

import { createContext, useContext, PropsWithChildren } from "react";
import { create, StoreApi, UseBoundStore } from "zustand";

// Define the type of the context
type LikesStore = {
  count: number;
  isLiked: boolean;
  initialize: (initialCount: number) => void;
  toggleLiked: () => void;
};

// Define the type of the context
type LikesContextType = UseBoundStore<StoreApi<LikesStore>>;

// Create the context
const likesContext = createContext<LikesContextType | undefined>(undefined);

// Create a hook to use the context
const useLikesContext = () => {
  const context = useContext(likesContext);
  if (context === undefined) {
    throw new Error("useLikesContext must be used within a LikesProvider");
  }
  return context;
};

// Create a provider
const LikesProvider = ({ children }: PropsWithChildren) => {
  // Zustand store
  const useLikesStore = create<LikesStore>((set) => ({
    count: 5,
    isLiked: false,
    initialize: (initialCount) => set({ count: initialCount }),
    toggleLiked: () =>
      set((state) => ({
        isLiked: !state.isLiked,
        count: state.isLiked ? state.count - 1 : state.count + 1,
      })),
  }));

  return (
    <likesContext.Provider value={useLikesStore}>
      {children}
    </likesContext.Provider>
  );
};

export { LikesProvider, useLikesContext };

Here's how it's used in different components within the Post component:

function Counter() {
  const { count } = useLikesContext()();
  return (
    <div className="my-3 flex h-3 items-center gap-2 px-2">
      <span className="text-xs text-zinc-300">{count} likes</span>
    </div>
  );
}
function LikeButton() {
  const { toggleLiked, isLiked } = useLikesContext()();
  return (
    <button
      className="group flex aspect-square select-none items-center justify-center gap-2 rounded-full bg-zinc-700 px-3 py-2 text-sm font-medium text-zinc-100"
      onClick={toggleLiked}
    >
      <span
        className={twJoin(
          "material-symbols-rounded scale-100 text-xl transition-all duration-150 ease-in-out group-active:scale-90",
          isLiked ? "filled text-pink-600" : "text-zinc-100"
        )}
      >
        favorite
      </span>
    </button>
  );
}

Is this approach appropriate, or should I revert to using useState or useReducer? Because I'm leaning toward the Zustand approach because it suits my preferences better.


Solution

  • You can, but your LikesProvider will create a new store each time you render it. You should also consider using the useStore hook in useLikesContext, rather than just extracting the whole store.

    N.b. 'zustand/context` mentioned in @Oktay's answer is deprecated, and will be removed in v5

    Adapting the documentation example:

    const LikesProvider = ({ children }: PropsWithChildren) => {
      const ref = useRef(create<LikesStore>((set) => ({
          count: 5,
          isLiked: false,
          initialize: (initialCount) => set({ count: initialCount }),
          toggleLiked: () =>
            set((state) => ({
              isLiked: !state.isLiked,
              count: state.isLiked ? state.count - 1 : state.count + 1,
            })),
        })));
      return (
        <likesContext.Provider value={ref.current}>
          {children}
        </likesContext.Provider>
      )
    }
    
    type UseLikesSelect<Selector> = Selector extends (state: LikesStore) => infer R ? R : never
    
    const useLikesContext = <Selector extends (state: LikesStore) => any>(selector: Selector): UseLikesSelect<Selector> => {
      const store = useContext(LikesContext)
      if (!store) {
        throw new Error('Missing LikesProvider')
      }
      return useStore(store, selector)
    }
    

    Which you would then use in components like

    function Counter() {
      const count = useLikesContext(s => s.count);
      return (
        <div className="my-3 flex h-3 items-center gap-2 px-2">
          <span className="text-xs text-zinc-300">{count} likes</span>
        </div>
      );
    }