React Hooks in SPFx: useEffect

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]);