- Published on
Understanding the SPFx Web Part Lifecycle in Depth
Every SPFx developer has hit a bug that turned out to be code running in the wrong lifecycle method β data fetching in render, DOM manipulation before the component mounts, or properties read before onInit completes.
Understanding exactly when each lifecycle hook fires, and what is available to you at that point, eliminates an entire class of subtle, hard-to-reproduce bugs.
This article walks through every lifecycle method in an SPFx web part, in execution order, with practical guidance on what belongs where.
πΊοΈ The Lifecycle at a Glance
An SPFx web part goes through four broad stages:
- Construction β the class is instantiated
- Initialization β
onInitcompletes async setup before any render - Rendering β
renderproduces the DOM output - Property changes & teardown β
onPropertyPaneFieldChanged,onDispose, etc.
The methods below are listed in the order the framework calls them.
ποΈ constructor()
The constructor runs first. At this point the SharePoint context is not yet available β this.context is undefined.
export default class MyWebPart extends BaseClientSideWebPart<IMyWebPartProps> {
constructor() {
super();
// β
Safe: initialise class-level variables
// β Do NOT call this.context here β it's undefined
}
}
Rule: Keep the constructor empty or use it only for field initialisation. Never call this.context, this.properties, or any async operation here.
βοΈ onInit(): Promise<void>
onInit is the designated place for all async setup. The framework awaits its return before calling render, so anything you need ready before the first paint goes here.
protected async onInit(): Promise<void> {
await super.onInit();
// β
Initialise PnPjs with SharePoint context
const sp = spfi().using(SPFx(this.context));
// β
Fetch configuration data before first render
this._config = await sp.web.lists.getByTitle('Config').items();
// β
Set up event subscriptions
this.context.application.navigatedEvent.add(this, this._onNavigated);
}
Common uses for onInit:
- Bootstrapping PnPjs or MSAL
- Fetching configuration or reference data
- Registering event listeners on
this.context.application - Setting up logging / telemetry clients
Rule: Always call await super.onInit() first. Everything that must be ready before the first render belongs here, not in render.
π¨ render(): void
render is called immediately after onInit resolves, and again every time the property pane saves a change. It is synchronous β do not put await calls directly inside it.
public render(): void {
const element: React.ReactElement<IMyProps> = React.createElement(MyComponent, {
description: this.properties.description,
context: this.context,
onConfigure: this._onConfigure.bind(this)
});
ReactDOM.render(element, this.domElement);
}
What belongs in render:
- Creating the root React element
- Passing properties and context down as props
- Rendering a loading skeleton if data is still being fetched in the React component
Rule: Keep render thin. Business logic, data fetching, and side effects belong in onInit or inside your React components via useEffect.
π onPropertyPaneFieldChanged(propertyPath, oldValue, newValue)
Called each time the user changes a field value in the property pane, even before they click Apply.
protected onPropertyPaneFieldChanged(
propertyPath: string,
oldValue: unknown,
newValue: unknown
): void {
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
if (propertyPath === 'listName' && newValue !== oldValue) {
// Re-fetch data when the target list changes
this._loadItems(newValue as string).then(() => this.render());
}
}
Rule: Use this hook to react to individual field changes β for example, populating a dependent dropdown based on the new value. For full re-renders on Apply, render is called automatically β you don't need to trigger it yourself.
πΎ onAfterPropertyPaneChangesApplied()
Fires after the user clicks Apply and all pending property changes have been committed to this.properties.
protected onAfterPropertyPaneChangesApplied(): void {
// All property changes are now committed
// Safe to persist or log the final values
console.log('Properties applied:', this.properties);
}
Use this for post-save side effects that should only run on explicit Apply, not on every keystroke.
π onBeforeSerialize(): IWebPartData
Called just before the web part's properties are serialised to the page. Lets you transform or trim property values before they are written to storage.
protected onBeforeSerialize(): IWebPartData {
// Strip runtime-only state before saving
delete (this.properties as any)._runtimeCache;
return super.onBeforeSerialize();
}
This is rarely needed, but important when you store ephemeral data on this.properties during runtime that must not be persisted.
π onAfterDeserialize(deserializedObject, dataVersion)
Called after the saved property bag is loaded from storage, giving you a chance to migrate or transform old property shapes.
protected onAfterDeserialize(
deserializedObject: IMyWebPartProps,
dataVersion: Version
): IMyWebPartProps {
// Migrate v1 property shape to v2
if (dataVersion.compare(new Version(1, 0)) === 0) {
deserializedObject.listName = deserializedObject['list'] ?? '';
}
return deserializedObject;
}
Always use this β rather than onInit β for property schema migrations across data versions.
π onDisplayModeChanged(oldDisplayMode)
Fires when the page switches between Edit and Read mode. Use this to swap read-only and interactive variants of your component.
protected onDisplayModeChanged(oldDisplayMode: DisplayMode): void {
super.onDisplayModeChanged(oldDisplayMode);
this.render(); // Re-render with the new display mode
}
Pass this.displayMode as a prop to your React component to conditionally show edit controls.
ποΈ onDispose(): void
Called when the web part is removed from the page or the page is closed. Clean up subscriptions, timers, and any external resources here.
protected onDispose(): void {
// β
Remove event listeners
this.context.application.navigatedEvent.remove(this, this._onNavigated);
// β
Unmount the React tree
ReactDOM.unmountComponentAtNode(this.domElement);
super.onDispose();
}
Rule: Always call super.onDispose() at the end. Failing to unmount the React tree here causes memory leaks in long-running SharePoint sessions.
π Lifecycle Execution Order β Summary
new MyWebPart()
βββ constructor()
βββ onInit() β async setup, awaited by framework
βββ render() β first paint
βββ [user edits property pane]
β βββ onPropertyPaneFieldChanged()
β βββ onAfterPropertyPaneChangesApplied()
βββ [page mode changes]
β βββ onDisplayModeChanged()
βββ [before save]
β βββ onBeforeSerialize()
βββ [web part removed]
βββ onDispose()
π¬ Observing the Lifecycle Yourself
The companion GitHub sample for this article implements a lifecycle logger web part that logs every hook β including timestamps and the values available at each stage β directly to the browser console.
Clone it, add it to your workbench, open DevTools, and you'll see the exact execution order in your own tenant.
View full SPFx project on GitHub:SPFx lifecycle logger web part β logs each hook with timestamps in the console
β Summary
constructorβ field initialisation only;this.contextis not available yet.onInitβ all async setup: PnPjs bootstrap, config fetch, event subscriptions. Awaited before first render.renderβ thin; create the React element and mount it. No async calls here.onPropertyPaneFieldChangedβ react to individual field edits, e.g. refresh a dependent dropdown.onAfterPropertyPaneChangesAppliedβ post-save side effects after the user clicks Apply.onBeforeSerialize/onAfterDeserializeβ transform properties before save or after load.onDisplayModeChangedβ switch between read and edit variants.onDisposeβ remove listeners, unmount React, release resources.
Getting these boundaries right means fewer mysterious bugs and cleaner, more maintainable web parts.
Happy coding!
Author
Ravichandran@Hi_Ravichandran
