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