State in SPFx: Global State (Context, Redux, Zustand)
Comparison and SPFx-specific considerations for Context, Redux, and Zustand with examples.
Choosing a global state solution
- Context: lightweight for shared configuration (theme, user). Combine with
useReducerfor structured updates. - Redux: predictable state container with devtools, actions, reducers, middleware. Great for complex flows.
- Zustand: minimal, ergonomic store with selectors; fewer boilerplate, excellent performance.
Context + Reducer pattern
type AppState = { theme: 'light' | 'dark'; user?: { name: string } };
type Action = { type: 'setTheme'; value: AppState['theme'] } | { type: 'setUser'; value: { name: string } };
function reducer(state: AppState, action: Action): AppState {
switch (action.type) {
case 'setTheme': return { ...state, theme: action.value };
case 'setUser': return { ...state, user: action.value };
default: return state;
}
}
const AppStateContext = React.createContext<{ state: AppState; dispatch: React.Dispatch<Action> } | null>(null);
export function AppStateProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, { theme: 'light' });
const value = useMemo(() => ({ state, dispatch }), [state]);
return <AppStateContext.Provider value={value}>{children}</AppStateContext.Provider>;
}
export function useAppState() {
const ctx = useContext(AppStateContext);
if (!ctx) throw new Error('AppStateProvider missing');
return ctx;
}
Redux example (minimal)
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
const slice = createSlice({
name: 'app',
initialState: { theme: 'light' as 'light' | 'dark' },
reducers: {
setTheme(state, action: PayloadAction<'light' | 'dark'>) { state.theme = action.payload; }
}
});
export const { setTheme } = slice.actions;
export const store = configureStore({ reducer: { app: slice.reducer } });
import { Provider, useDispatch, useSelector } from 'react-redux';
export function App() {
return <Provider store={store}><WebPartRoot /></Provider>;
}
function WebPartRoot() {
const theme = useSelector((s: any) => s.app.theme);
const dispatch = useDispatch();
return <button onClick={() => dispatch(setTheme(theme === 'light' ? 'dark' : 'light'))}>Toggle</button>;
}
Zustand example
import { create } from 'zustand';
type AppStore = { theme: 'light' | 'dark'; setTheme: (t: 'light' | 'dark') => void };
export const useAppStore = create<AppStore>(set => ({
theme: 'light',
setTheme: (t) => set({ theme: t })
}));
function WebPartRoot() {
const theme = useAppStore(s => s.theme);
const setTheme = useAppStore(s => s.setTheme);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}
Best Practices
- Use selectors to limit re-renders (Redux
useSelector, Zustand slice selectors). - Memoize provider values and avoid passing new objects each render.
- Split stores/contexts by domain to reduce churn.
- Avoid putting large lists or volatile data in global state unless necessary; keep them in local state or query layer.
Anti‑Patterns
- Oversized monolithic context causing broad re-renders.
- Storing ephemeral UI flags globally when they’re local.
- Mutating state in reducers/stores; always return new state.
SPFx-specific notes
- Tie global state to environment (tenant/site URLs) via initialization; avoid hardcoding.
- For property pane changes affecting global behavior, dispatch concise actions and debounce if needed.