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