React Hooks in SPFx: useContext

React Hooks in SPFx: useContext

Theme, user info, and global state in SPFx via Context; composition and provider design.

Why useContext in SPFx

Share global data (theme, user info, environment, permissions) across components without prop drilling. Wrap providers at your web part root.

Best Practices

1) Strongly typed context with sane defaults

type ThemeInfo = { name: 'light' | 'dark'; primary: string };
const ThemeContext = React.createContext<ThemeInfo | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
	const [theme, setTheme] = useState<ThemeInfo>({ name: 'light', primary: '#2b6cb0' });
	const value = useMemo(() => theme, [theme]);
	return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
	const t = useContext(ThemeContext);
	if (!t) throw new Error('ThemeContext missing provider');
	return t;
}

2) Memoize provider values

Wrap provider value in useMemo and avoid recreating functions inline to reduce re-renders.

3) Split contexts by domain

Avoid oversized context objects. Use separate providers (Theme/User/Settings) to minimize unnecessary updates.

Anti‑Patterns

❌ Prop drilling global data

If many components need the same data, prefer context over long prop chains.

❌ Unstable provider value

Creating a new object each render ({...}) without memoization causes all consumers to re-render.

❌ Using context for rapidly changing local state

Keep fast-changing per-component state in useState/useReducer rather than context.

Async initialization patterns

Load initial provider data asynchronously with cancellation and a small loading state.

type UserInfo = { displayName: string; upn: string };
const UserContext = React.createContext<UserInfo | null>(null);

export function UserProvider({ children }: { children: React.ReactNode }) {
	const [user, setUser] = useState<UserInfo | null>(null);
	const [loading, setLoading] = useState(false);
	const [error, setError] = useState<string | null>(null);

	useEffect(() => {
		const ac = new AbortController();
		(async () => {
			try {
				setLoading(true);
				const res = await fetch(`${siteUrl}/_api/me`, { signal: ac.signal });
				const json = await res.json();
				setUser(json as UserInfo);
			} catch (e: any) {
				if (e.name !== 'AbortError') setError(e.message);
			} finally {
				setLoading(false);
			}
		})();
		return () => ac.abort();
	}, []);

	const value = useMemo(() => user, [user]);
	if (loading) return <Spinner />;
	if (error) return <MessageBar intent="error">{error}</MessageBar>;
	return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

export function useUser() {
	const u = useContext(UserContext);
	if (!u) throw new Error('UserContext missing provider');
	return u;
}

SPFx theme subscription

Subscribe to theme changes once and update provider state.

export function ThemeProvider({ themeProvider, children }: { themeProvider: any; children: React.ReactNode }) {
	const [theme, setTheme] = useState<ThemeInfo>({ name: 'light', primary: '#2b6cb0' });
	useEffect(() => {
		function onChange(next: any) {
			setTheme(prev => ({ ...prev, name: next.isInverted ? 'dark' : 'light', primary: next.palette?.themePrimary ?? prev.primary }));
		}
		themeProvider.on('change', onChange);
		return () => themeProvider.off('change', onChange);
	}, [themeProvider]);
	const value = useMemo(() => theme, [theme]);
	return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}