Bonus: Migrate SPFx Class → Function Components (Hooks)

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: componentDidMountuseEffect([]), updates → useEffect([deps]), cleanup → return function.
  • Move class state to useState or useReducer for complex workflows.
  • Replace instance fields with useRef for 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 Context in a provider at the container level; presentational components stay pure.

Anti‑Patterns

  • One giant useEffect doing 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]);