Skip to main content

πŸ—️ Salesforce LWC Lifecycle Hooks: The Complete Guide

Updated Add a comment
πŸ’¬ In plain words: Every Lightning Web Component (LWC) goes through the same life stages. It gets created, added to the page, drawn on the screen, and later removed. LWC gives you a lifecycle hook for each stage. constructor = born. connectedCallback = moved into the page. renderedCallback = painted on the screen. disconnectedCallback = removed from the page. Always put your code in the earliest hook that has what your code needs.

Put code in the wrong hook and you get bugs that are hard to track down: values that show up as undefined, a browser tab that freezes, or listeners that pile up every time the component loads again. This post walks through each hook in the order it runs, shows what belongs in each one, and covers the mistakes that come up most in real projects and interviews.

⚡ 1-Minute Summary (Key Points)

  • Never read @api values in the constructor. The parent hasn't passed them yet.
  • Use connectedCallback to set up subscriptions and run the first data load.
  • renderedCallback fires after every render, not just the first one. Use a boolean guard so one-time work runs only once.
  • Clean up event listeners, timers, and subscriptions in disconnectedCallback to avoid memory leaks.
  • errorCallback on a parent catches errors from its child components, never from itself.
πŸ“Œ Example: Setting Up a Chart Library

A developer sets up a chart library inside connectedCallback. It breaks right away because the canvas element isn't on the screen yet. The fix is to move the setup to renderedCallback and use a hasRendered flag so it runs only once. Then remove the window resize listener in disconnectedCallback so nothing is left behind.

⏱️ The Exact Order of Operations

When a component is added to a page, its hooks run in this order:

  • 1. constructor: Runs first, when the component is created. Nothing is rendered yet, and the parent hasn't passed any @api values. Runs parent to child.
  • 2. connectedCallback: Runs when the component is inserted into the DOM. @api values are set now, so this is the right place for subscriptions and the first data load. Runs parent to child. It can also run more than once. For example, when a list is reordered, the component is removed and inserted again, and this hook fires again.
  • 3. render: Picks which HTML template to show. You rarely need to override it, and it isn't technically a lifecycle hook.
  • 4. renderedCallback: Runs after the component and all its children finish rendering, so the DOM exists now. Runs child to parent. Warning: It fires after EVERY render, so guard any one-time setup.
  • 5. disconnectedCallback: Runs when the component is removed from the DOM. Unsubscribe and remove listeners here. Runs parent to child.
  • 6. errorCallback: Runs on a parent when one of its child components throws an error. Use it to build an error boundary.
🧠 Born, moved in, painted, moved out: constructor ➔ connectedCallback ➔ renderedCallback (fires often) ➔ disconnectedCallback.

πŸ”€ Parent vs. Child Order

With nested components, each phase runs in a fixed direction. Knowing this helps you avoid timing bugs, like a parent trying to use a child that hasn't rendered yet.

Phase Direction Order
Creation Parent → Child Parent constructor() → Child constructor()
Inserted into DOM Parent → Child Parent connectedCallback() → Child connectedCallback()
Rendering Child → Parent Child renderedCallback() → Parent renderedCallback()
Removal Parent → Child Parent disconnectedCallback() → Child disconnectedCallback()

The key point: the parent's renderedCallback runs last, after every child has rendered. If a parent needs to work with a child element, wait until then.

🧭 360 Card — Lifecycle Hooks

🎯 Rule: Born in constructor. In the page at connectedCallback. Painted at renderedCallback. Gone at disconnectedCallback.

πŸš€ Gain: Four clear moments, each with an obvious job: defaults, setup, DOM work, and cleanup.

πŸ’° Price: renderedCallback fires after every reactive change. Any unguarded work there runs again and again.

πŸ›‘ Limits: @api values are not available in the constructor. errorCallback catches errors from child components, but never the component's own errors.

πŸ”„ Scale: In a console app, recordId can change while the component stays on the page. Use @wire or an @api setter to react to the change. Don't count on connectedCallback to load the data again.

πŸ› ️ Each Hook in Code

Here's one component that uses each hook the right way. It listens to a Lightning Message Service (LMS) channel, sets up a chart once, and cleans up when it leaves the page.

Step 1: Create with constructor()
Always call super() first. Only set simple defaults here.
import { LightningElement, api, wire } from 'lwc';
import { subscribe, unsubscribe, MessageContext } from 'lightning/messageService';
import RECORD_SELECTED from '@salesforce/messageChannel/Record_Selected__c';

export default class LifecycleDemo extends LightningElement {
    @api recordId;
    @wire(MessageContext) messageContext;
    subscription = null;
    hasRendered = false;

    constructor() {
        super(); // Must be the first line
        // this.recordId is still undefined here,
        // and the template hasn't rendered, so there is nothing to query.
    }

    handleMessage(message) {
        // Update the component with the new record
    }

    // The hooks below go inside this class
}
Step 2: Set Up with connectedCallback()
@api values are ready. Subscribe to channels and load starting data here. This hook can run more than once, so check before you subscribe.
    connectedCallback() {
        // this.recordId is available now
        if (!this.subscription) {
            this.subscription = subscribe(
                this.messageContext,
                RECORD_SELECTED,
                (message) => this.handleMessage(message)
            );
        }
        // Also a good place for an imperative Apex call
    }
Step 3: Work with the DOM in renderedCallback()
Runs after every render. Use a flag so one-time setup runs only once.
    renderedCallback() {
        if (this.hasRendered) {
            return; // One-time setup is already done
        }

        const container = this.template.querySelector('.chart-container');
        if (container) {
            this.hasRendered = true;
            // Safe to set up a chart or other third-party library here
        }
    }
Step 4: Clean Up in disconnectedCallback()
Runs when the component leaves the page. Undo everything you set up in connectedCallback().
    disconnectedCallback() {
        unsubscribe(this.subscription);
        this.subscription = null;
        // Also remove window listeners and clear any timers here
    }
Step 5: Catch Child Errors with errorCallback()
Add this to a parent component that wraps a section of the page. It catches errors from its children, not from itself.
    error;

    errorCallback(error, stack) {
        this.error = error; // Switch the template to a fallback message
        console.error(error.message, stack);
        // Send the details to your logging service
    }

⚠️ Common Traps and Best Practices

⚠ INTERVIEW TRAP: The Infinite Render Loop
renderedCallback fires after EVERY render, not just once. If you change a field that the template uses inside this hook, the component renders again, which calls renderedCallback again, and the loop never ends. The browser tab freezes. This is the most common lifecycle bug. Only change state here behind a guard that makes sure it happens once.
⚠ CONSTRUCTOR TRAP: Reading Too Early
In the constructor(), the component isn't on the page yet. The template hasn't rendered, so this.template.querySelector() finds nothing, and @api properties don't have the parent's values yet. Move any logic that needs them to connectedCallback().
Core Rule: Use constructor() only for super() and simple defaults. Use connectedCallback() for data loading and subscriptions. Use renderedCallback() only for DOM work, guarded by a flag.
  • Always Clean Up: Every listener, timer, or subscription you add in connectedCallback() needs a matching cleanup in disconnectedCallback(). Otherwise, old handlers keep running and memory leaks build up.
  • Prefer Template Directives: To show or hide markup, use lwc:if, lwc:elseif, and lwc:else. Override render() only when you need to switch between completely different HTML template files.

πŸ™‹ Core Q&A & Scenarios

Q: What are the classic LWC lifecycle mistakes, and how do you fix them?
🎯 Say this first: Heavy work in renderedCallback (it runs again and again), DOM access in connectedCallback (it's too early), and no cleanup in disconnectedCallback.

Detailed breakdown:

  • Mistake 1: Reading @api values in the constructor. They aren't set yet. Move that code to connectedCallback.
  • Mistake 2: One-time setup in renderedCallback without a boolean guard. The setup runs on every render and can break third-party libraries.
  • Mistake 3: Changing reactive state inside renderedCallback. That triggers another render and causes an infinite loop.
  • Mistake 4: Adding a window listener or timer in connectedCallback and never removing it in disconnectedCallback. It keeps running after the component is gone, which leaks memory and creates duplicate handlers when the component comes back. Treat LMS the same way: Salesforce's own examples always unsubscribe in disconnectedCallback.
  • Mistake 5: Expecting a child's DOM to be ready in the parent's connectedCallback. The children haven't rendered yet.
Q: How does errorCallback work, and what does a production error boundary look like?

A: errorCallback(error, stack) on a parent catches errors thrown by any component below it in the tree. It covers errors in their lifecycle hooks and in event handlers declared in their HTML template, like onclick={handleClick}. It does NOT catch the parent's own errors. It also misses errors from handlers you add in JavaScript with addEventListener() and errors from async code like promises. Handle those with try/catch or .catch().

A production error boundary wraps a section of the page. When it catches an error, it switches to a fallback template, sends the error and stack to a logging service, and gives the user a way to try again. Without boundaries, one broken child can take down a whole section of the page.

Q: A component loads data based on @api recordId, and that value can CHANGE while the component stays on the page (like in Salesforce console apps). Where does the load logic go?

A: Not in connectedCallback. That hook runs only when the component is inserted. If the user opens a different record in a console app, recordId changes, but your component keeps showing old data.

Instead, use an @wire with '$recordId' as a parameter, so it reloads on its own when the value changes. Or use an @api setter that runs an imperative fetch only when the value is actually different (add that check so you don't loop). Keep in mind the setter can run before connectedCallback when the parent first sets the value.

Q: Can connectedCallback run more than once?

A: Yes. If the component is removed from the DOM and inserted again, for example when a list is reordered, connectedCallback runs again. That's why setup code there should check whether it already ran, and why every setup step needs a matching cleanup in disconnectedCallback.

Q: Why not do everything in the constructor?

A: In the constructor, the component isn't on the page yet. @api values are undefined, and there's no DOM to work with. connectedCallback runs after the component is inserted, so it's the safe place to load data. Golden Rule: In the constructor, don't touch anything outside the component.

✅ Summary

Each lifecycle hook has one job. Set defaults in the constructor, load data and subscribe in connectedCallback, do guarded DOM work in renderedCallback, and clean up in disconnectedCallback. Add errorCallback to a parent so one broken child can't take down the page. Follow these rules, and your components stay fast, free of leaks, and easy to debug.

Was this article helpful?

Have a question or spotted a mistake? Ask here about this article

Found this guide useful? Buy me a coffee (opens in a new tab)

Comments

Questions, corrections and your own experience are all welcome.