logo
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:

  1. Construction β€” the class is instantiated
  2. Initialization β€” onInit completes async setup before any render
  3. Rendering β€” render produces the DOM output
  4. 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

GitHub

βœ… Summary

  • constructor β€” field initialisation only; this.context is 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!

Ad image