How to initialise global state by React props? #3361
Replies: 3 comments 11 replies
-
|
Zustand store is the "external" state from the React perspective. To synchronize with external state, one canonical solution is to use |
Beta Was this translation helpful? Give feedback.
-
Solution: Initialize Zustand Store from PropsYou're right - mutating state during render is bad. Here are the recommended patterns: 1. Store Factory Pattern (Recommended for Libraries)import { createStore, useStore } from "zustand";
import { createContext, useContext, useRef } from "react";
// Create store factory
const createRecorderStore = (initialOptions) =>
createStore((set) => ({
options: initialOptions,
setOptions: (opts) => set({ options: opts }),
}));
// Context for the store instance
const RecorderContext = createContext(null);
// Provider component
export const VideomailRecorder = ({ options, children }) => {
const storeRef = useRef();
// Create store once with initial props
if (!storeRef.current) {
storeRef.current = createRecorderStore(options);
}
return (
<RecorderContext.Provider value={storeRef.current}>
{children}
</RecorderContext.Provider>
);
};
// Hook to use the store
export const useRecorderStore = (selector) => {
const store = useContext(RecorderContext);
return useStore(store, selector);
};2. Usage in Child Componentsfunction RecordButton() {
const options = useRecorderStore((s) => s.options);
return <button>{options.buttonText}</button>;
}
// In your app
<VideomailRecorder options={{ buttonText: "Record" }}>
<RecordButton />
</VideomailRecorder>Why this works:
This is the official recommended pattern from Zustand docs for React context integration. |
Beta Was this translation helpful? Give feedback.
-
|
You're right that mutating state during render is a bad idea. For a component library where the root component receives config props, the cleanest Zustand pattern is a store factory with React context. Recommended: Store factory + context provider// store.ts
import { createStore } from "zustand";
export interface RecorderState {
options: JSONObject;
isRecording: boolean;
// ... other state
setOptions: (options: JSONObject) => void;
startRecording: () => void;
stopRecording: () => void;
}
export const createRecorderStore = (initialOptions: JSONObject = {}) =>
createStore<RecorderState>((set) => ({
options: initialOptions,
isRecording: false,
setOptions: (options) => set({ options }),
startRecording: () => set({ isRecording: true }),
stopRecording: () => set({ isRecording: false }),
}));
export type RecorderStore = ReturnType<typeof createRecorderStore>;// context.tsx
import { createContext, useContext, useRef } from "react";
import { useStore } from "zustand";
import { createRecorderStore, RecorderState, RecorderStore } from "./store";
const RecorderContext = createContext<RecorderStore | null>(null);
export function RecorderProvider({
options,
children,
}: {
options?: JSONObject;
children: React.ReactNode;
}) {
const storeRef = useRef<RecorderStore | null>(null);
if (storeRef.current === null) {
// Create store once with initial props — not a mutation during render,
// this is lazy initialization (same pattern as useRef)
storeRef.current = createRecorderStore(options);
}
return (
<RecorderContext.Provider value={storeRef.current}>
{children}
</RecorderContext.Provider>
);
}
export function useRecorderStore<T>(selector: (state: RecorderState) => T): T {
const store = useContext(RecorderContext);
if (!store) {
throw new Error("useRecorderStore must be used within <RecorderProvider>");
}
return useStore(store, selector);
}// VideomailRecorder.tsx
export interface VideomailRecorderProps {
options?: JSONObject;
children?: React.ReactNode;
}
export const VideomailRecorder = ({ options, children }: VideomailRecorderProps) => {
return (
<RecorderProvider options={options}>
<RecorderInner />
{children}
</RecorderProvider>
);
};
function RecorderInner() {
const isRecording = useRecorderStore((s) => s.isRecording);
const start = useRecorderStore((s) => s.startRecording);
return (
<div>
<h1>{isRecording ? "Recording..." : "Ready"}</h1>
<button onClick={start}>Start</button>
</div>
);
}Why this pattern works for component libraries
If options can change after mountIf the parent can pass new options that should update the store: import { useEffect } from "react";
export function RecorderProvider({
options,
children,
}: {
options?: JSONObject;
children: React.ReactNode;
}) {
const storeRef = useRef<RecorderStore | null>(null);
if (storeRef.current === null) {
storeRef.current = createRecorderStore(options);
}
// Sync prop changes to store (outside of render)
useEffect(() => {
if (options) {
storeRef.current?.getState().setOptions(options);
}
}, [options]);
return (
<RecorderContext.Provider value={storeRef.current}>
{children}
</RecorderContext.Provider>
);
}This is the pattern Zustand's docs recommend for library authors — see the Extracting the store to a custom hook with vanilla store section. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Asking for a solution, how to implement this best in Zustand.
In short, I'm building a new component library (not an app!) with a root component called Recorder. I would like to initialise a global state with options given as react props to the root component in Zustand:
I'm not sure. I believe mutating state during render is a bad idea. Suggestions welcome.
Beta Was this translation helpful? Give feedback.
All reactions