Bonus: Migrate SPFx Class → Function Components (Hooks)
Approach, pitfalls, and examples for updating SPFx class components to function components with hooks.
Migration strategy
- Map lifecycle methods to effects:
componentDidMount→useEffect([]), updates →useEffect([deps]), cleanup → return function. - Move class
statetouseStateoruseReducerfor complex workflows. - Replace instance fields with
useReffor mutable, non-rendering values. - Use Context for shared dependencies (e.g., SPFx
Context) instead of prop drilling.
Lifecycle mapping
// componentDidMount
React.useEffect(() => {
// run once after mount
init();
}, []);
// componentDidUpdate(prevProps)
React.useEffect(() => {
// respond to siteUrl changes
refresh(siteUrl);
}, [siteUrl]);
// componentWillUnmount
React.useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
Example: class → hooks
type Props = { siteUrl: string };
type State = { items: Array<{ id: number; title: string }>; loading: boolean; error?: string };
export class TasksPane extends React.Component<Props, State> {
private ac?: AbortController;
state: State = { items: [], loading: false };
componentDidMount() { this.load(); }
componentDidUpdate(prev: Props) { if (prev.siteUrl !== this.props.siteUrl) this.load(); }
componentWillUnmount() { this.ac?.abort(); }
async load() {
try {
this.ac?.abort(); this.ac = new AbortController();
this.setState({ loading: true, error: undefined });
const res = await fetch(`${this.props.siteUrl}/_api/web/lists/getbytitle('Tasks')/items`, { signal: this.ac.signal });
const json = await res.json();
this.setState({ items: json.value, loading: false });
} catch (e: any) {
if (e.name !== 'AbortError') this.setState({ error: e.message, loading: false });
}
}
render() { /* ... */ return null; }
}
type Props = { siteUrl: string };
type TasksResponse = { value: { id: number; title: string }[] };
export function TasksPane({ siteUrl }: Props) {
const { data, loading, error } = useCancelableFetch<TasksResponse>(
`${siteUrl}/_api/web/lists/getbytitle('Tasks')/items`,
[siteUrl]
);
const items = React.useMemo(() => data?.value ?? [], [data]);
if (loading) return <Spinner />;
if (error) return <MessageBar intent="error">{error}</MessageBar>;
return <ItemsList items={items} />;
}
SPFx migration notes
- Property pane updates: effects depending on web part properties will re-run often; debounce heavy work.
- Theme changes: subscribe in an effect and clean up on unmount.
- Context: keep SPFx
Contextin a provider at the container level; presentational components stay pure.
Anti‑Patterns
- One giant
useEffectdoing multiple responsibilities; split per concern. - Storing derived state (e.g., filtered lists) instead of deriving via
useMemo. - Forgetting cleanup for subscriptions/timers → leaks.
- Async without cancellation → setState after unmount.
Async & cancellation
Prefer abortable fetch patterns and cleanups:
React.useEffect(() => {
const ac = new AbortController();
(async () => {
const res = await fetch(url, { signal: ac.signal });
// ...
})();
return () => ac.abort();
}, [url]);