State in SPFx: Local Component State

State in SPFx: Local Component State

Guidance for local state usage, trade-offs, and patterns within SPFx web parts.

When to use local state

Use local state for UI concerns that are scoped to a single component: inputs, toggles, small objects. Prefer derived values over storing duplicates and keep updates immutable.

Best Practices

1) Keep state minimal and derive outputs

const [items, setItems] = useState<string[]>([]);
const count = items.length; // derive instead of storing

2) Immutable updates

const [ui, setUi] = useState({ theme: 'light', busy: false });
const setBusy = (next: boolean) => setUi(prev => ({ ...prev, busy: next }));

3) Lift state up when multiple children need it

If siblings read/update the same data, lift the state to their common parent and pass props down.

4) Switch to useReducer for complex transitions

Multiple related fields and coordinated transitions benefit from a reducer.

Anti‑Patterns

❌ Duplicating derived data

Avoid storing values like items.length or computed flags; derive them.

❌ Mutating state

Never mutate objects/arrays in place; React may not re-render.

❌ Overusing local state for app-wide data

Global concerns (theme, user, environment) belong in context/global state.

Controlled vs uncontrolled inputs

Prefer controlled inputs for predictable state, but uncontrolled can be fine for simple forms. In SPFx property pane scenarios, controlled inputs help synchronize with web part properties.

const [title, setTitle] = useState('');
return <input value={title} onChange={e => setTitle(e.target.value)} />;

Async local state patterns

Use loading and error flags and cancel async work on unmount.

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [items, setItems] = useState<any[]>([]);

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