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.
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>
);
}