State in SPFx: Global State (Context, Redux, Zustand)

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 useReducer for 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.