skip to Main Content

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.

2

Answers


  1. Zustand has its own Provider functionality. zustand/context

    https://docs.pmnd.rs/zustand/previous-versions/zustand-v3-create-context

    From the example:

    import create from 'zustand'
    import createContext from 'zustand/context'
    
    const { Provider, useStore } = createContext()
    
    const createStore = () => create(...)
    
    const App = () => (
      <Provider createStore={createStore}>
        ...
      </Provider>
    )
    
    const Component = () => {
      const state = useStore()
      const slice = useStore(selector)
      ...
    

    If it does not fit you, you can use your implementation as well. With few changes.

    If you create a store inside a component, you have to memoize it, in order to create it only once and not on each render.

    const useLikesStore = useMemo(() => create(...), [])  // create the store only once
    

    Better change your custom hook into this:

    const useLikesContext = (selector) => {  // use a selector
      const useStore = useContext(likesContext);
      if (useStore === undefined) {
        throw new Error("useLikesContext must be used within a LikesProvider");
      }
      return useStore(selector);  // call it here, instead of returning the hook
    };
    

    It is a hook, that returns another hook. This ensures that the returned hook will be called only at top level.

    const count = useLikesContext((store) => store.count);
    

    I don’t see selectors in your example. If you don’t use them, no need to use zustand, context will do the same thing.

    Login or Signup to reply.
  2. 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>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search