React Hooks in SPFx: useEffect
SPFx-specific guidance for useEffect, dependency arrays, cleanup functions, and real-world API calls.
Why useEffect in SPFx
useEffect handles side-effects: network calls, subscriptions, timers, and DOM interactions. In SPFx, it’s common for effects to depend on web part properties, theme, context, or environment variables.
Best Practices
1) Declare all dependencies
Missing dependencies cause stale values and bugs. Include props/context values you read inside the effect.
function List({ siteUrl, listTitle }: { siteUrl: string; listTitle: string }) {
const [items, setItems] = useState<any[]>([]);
useEffect(() => {
fetch(`${siteUrl}/_api/web/lists/getbytitle('${listTitle}')/items`)
.then(r => r.json()).then(d => setItems(d.value));
}, [siteUrl, listTitle]);
return <ItemsList items={items} />;
}
2) Cleanup subscriptions and timers
Always return a cleanup function for events/timers.
useEffect(() => {
const handle = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(handle);
}, []);
3) Split effects by responsibility
Prefer multiple focused effects over a single monolithic one.
// Load data
useEffect(() => { /* fetch */ }, [siteUrl]);
// Subscribe to theme changes
useEffect(() => { /* onThemeChange */ return () => {/* unsubscribe */} }, [themeProvider]);
Anti‑Patterns
❌ Missing dependencies
useEffect(() => {
// uses listTitle, but not listed in deps
fetch(`${siteUrl}/_api/...${listTitle}`);
}, [siteUrl]);
Fix: add listTitle to the deps.
❌ Doing heavy work every render
Avoid effects that run on every render unless necessary. Constrain with a correct deps array.
❌ Side-effects in render
Never run side-effects directly inside the component body; use effects.
Async with cancellation (unmount-safe)
Use AbortController to avoid calling setState after unmount.
useEffect(() => {
const ac = new AbortController();
(async () => {
try {
setLoading(true);
const res = await fetch(`${siteUrl}/_api/...`, { signal: ac.signal });
const json = await res.json();
setItems(json.value);
} catch (e: any) {
if (e.name !== 'AbortError') setError(e.message);
} finally {
setLoading(false);
}
})();
return () => ac.abort();
}, [siteUrl]);
SPFx-specific subscriptions
Theme changes
If you use a ThemeProvider/context, subscribe once and clean up.
useEffect(() => {
function onThemeChange(next: Theme) { setUi(prev => ({ ...prev, theme: next.name })); }
themeProvider.on('change', onThemeChange);
return () => themeProvider.off('change', onThemeChange);
}, [themeProvider]);
Property pane interactions
For async property-driven changes, separate effects by property and debounce where appropriate.
useEffect(() => {
// react to listTitle change only
// fetch items for the selected list
}, [listTitle]);