Apex • LWC •

LWC Lifecycle Hooks, Rendering and Reactivity Explained with Practical Example

khumed Khatib 5 min read

Article 4: LWC Lifecycle, Rendering and Reactivity – Understanding What Happens Behind the Scenes

Salesforce Lightning Web Components (LWC) is not just about writing HTML, JavaScript, and Apex. To build efficient components, a developer needs to understand what happens when a component is created, added to the DOM, updated, rendered, and eventually removed.

I have seen developers use connectedCallback() and renderedCallback() interchangeably, especially when initializing data or accessing DOM elements. Both have their own purpose, and using them incorrectly can lead to unnecessary server calls, performance issues, and even infinite rendering loops.

In this article, let's understand the LWC lifecycle, reactivity, rendering, and cleanup with a practical dashboard example.

1. Understanding the LWC Component Lifecycle

Every LWC component follows a lifecycle. Think of it like the journey of a component from creation to destruction.

Salesforce provides different lifecycle hooks that allow us to execute code at specific stages.

Important LWC Lifecycle Hooks

1. constructor()

The constructor is called when the component instance is created.

  • It is the first lifecycle hook.
  • Used for basic initialization.
  • You must call super() before accessing this.
  • Do not access child elements or perform DOM operations here.
constructor() {
    super();
    console.log('Component created');
}

2. connectedCallback()

This hook executes when the component is inserted into the DOM.

Typical use cases:

  • Initializing component data.
  • Calling Apex methods.
  • Registering event listeners.
  • Setting up subscriptions.
connectedCallback() {
    console.log('Component added to DOM');
    this.loadDashboardData();
}
Important: connectedCallback() can execute multiple times if a component is removed and added to the DOM again. Don't assume it runs only once during the entire lifetime of the component.

3. render()

The render() lifecycle method determines which HTML template should be displayed.

render() {
    return this.showAdminView
        ? adminTemplate
        : userTemplate;
}

Most components don't need to override this method. It is useful when a component needs to switch between multiple HTML templates dynamically.

4. renderedCallback()

This hook executes after the component has rendered its DOM.

It is useful when you need to interact with DOM elements after rendering.

renderedCallback() {
    console.log('Component rendered');
}

It can execute multiple times whenever the component rerenders. Updating reactive properties inside this hook without proper control can create an infinite rendering loop.

5. disconnectedCallback()

This hook executes when the component is removed from the DOM.

It is extremely important for cleanup.

disconnectedCallback() {
    console.log('Component removed from DOM');
}

Typical use cases include removing event listeners, clearing timers, unsubscribing from message channels, and releasing resources.

6. errorCallback(error, stack)

This hook catches errors from descendant components in the component hierarchy.

errorCallback(error, stack) {
    console.error('Child component error:', error);
    console.error('Error stack:', stack);
}

It is not a universal JavaScript try/catch mechanism. It handles errors thrown during descendant component lifecycle execution and rendering.

2. Understanding Reactivity in LWC

Reactivity is what makes LWC interesting. When a reactive property changes, LWC can automatically update the component's UI.

import { LightningElement } from 'lwc';

export default class Counter extends LightningElement {

    count = 0;

    handleIncrement() {
        this.count++;
    }
}

HTML:

<template>
    <p>Current Count: {count}</p>

    <lightning-button
        label="Increment"
        onclick={handleIncrement}>
    </lightning-button>
</template>

Whenever the user clicks the button, the count changes and the UI updates.

Do we still need @track?

This is a common interview question.

In modern LWC, primitive fields are reactive, and assigning a new object or array reference also triggers reactivity.

userName = 'Khumed';

userName = 'Salesforce Developer';

For objects and arrays, assigning a new reference is the recommended approach.

user = {
    name: 'Khumed',
    role: 'Developer'
};

updateUser() {
    this.user = {
        ...this.user,
        role: 'Lead Developer'
    };
}

The @track decorator is still useful when you need to observe deep mutations inside plain objects and arrays.

import { LightningElement, track } from 'lwc';

export default class UserDetails extends LightningElement {

    @track user = {
        name: 'Khumed',
        city: 'Nagpur'
    };

    updateCity() {
        this.user.city = 'Pune';
    }
}

3. Getters in LWC – Keeping Logic Out of HTML

Getters help us calculate values dynamically. Instead of writing complex expressions in HTML, we can keep the logic in JavaScript.

totalAmount = 5000;
discount = 500;

get finalAmount() {
    return this.totalAmount - this.discount;
}

HTML:

<template>
    <p>Final Amount: {finalAmount}</p>
</template>

Getters are evaluated during rendering when their values are needed. Avoid putting expensive calculations or server calls inside getters. A getter should ideally be a simple, predictable calculation.

4. Conditional Rendering – if:true vs lwc:if

Sometimes we need to display different UI sections based on a condition.

For modern LWC development, prefer lwc:if, lwc:elseif, and lwc:else.

<template>

    <template lwc:if={isLoading}>
        <lightning-spinner
            alternative-text="Loading">
        </lightning-spinner>
    </template>

    <template lwc:elseif={hasData}>
        <p>Dashboard data loaded successfully.</p>
    </template>

    <template lwc:else>
        <p>No dashboard data available.</p>
    </template>

</template>

This makes templates cleaner and easier to maintain.

5. Avoiding Infinite Rendering Loops

This is one of the most important things to understand in LWC.

Consider the following example:

renderedCallback() {
    this.count = this.count + 1;
}

Why is this a problem?

  1. Component renders.
  2. renderedCallback() executes.
  3. The reactive property count changes.
  4. LWC schedules another render.
  5. renderedCallback() executes again.
  6. The cycle continues.

How to avoid it?

If you must perform one-time initialization inside renderedCallback(), use a guard.

hasRendered = false;

renderedCallback() {

    if (this.hasRendered) {
        return;
    }

    this.hasRendered = true;

    console.log('One-time DOM initialization');
}

However, don't move every initialization task into renderedCallback(). Use the correct lifecycle hook for the job.

  • Data initialization: connectedCallback()
  • DOM-dependent initialization: renderedCallback()
  • Resource cleanup: disconnectedCallback()

6. Practical Example: Building an Efficient LWC Dashboard

Now let's put these concepts together.

Imagine we are building a Salesforce dashboard that displays:

  • Total Accounts
  • Total Opportunities
  • Open Cases
  • Current browser width

Our requirements:

  1. Load dashboard data when the component is initialized.
  2. Avoid repeated Apex calls.
  3. Display a loading spinner.
  4. Update browser width when the window is resized.
  5. Remove the event listener when the component is destroyed.

Step 1: Apex Controller

Create an Apex class named DashboardController.

public with sharing class DashboardController {

    @AuraEnabled(cacheable=true)
    public static Map<String, Integer> getDashboardData() {

        Map<String, Integer> result =
            new Map<String, Integer>();

        result.put(
            'accounts',
            [SELECT COUNT() FROM Account]
        );

        result.put(
            'opportunities',
            [SELECT COUNT() FROM Opportunity]
        );

        result.put(
            'cases',
            [SELECT COUNT() FROM Case
             WHERE IsClosed = false]
        );

        return result;
    }
}

This Apex method returns the dashboard statistics in a single response.

Step 2: JavaScript Controller

Create salesforceDashboard.js.

import { LightningElement } from 'lwc';

import getDashboardData
    from '@salesforce/apex/DashboardController.getDashboardData';

export default class SalesforceDashboard
    extends LightningElement {

    dashboardData = {};

    isLoading = true;

    errorMessage;

    viewportWidth = 0;

    hasLoadedData = false;

    connectedCallback() {

        this.viewportWidth = window.innerWidth;

        window.addEventListener(
            'resize',
            this.handleResize
        );

        if (!this.hasLoadedData) {
            this.loadDashboardData();
        }
    }

    async loadDashboardData() {

        this.isLoading = true;

        try {

            const result = await getDashboardData();

            this.dashboardData = result;

            this.hasLoadedData = true;

            this.errorMessage = undefined;

        } catch (error) {

            this.errorMessage =
                error?.body?.message ||
                'Unable to load dashboard data';

        } finally {

            this.isLoading = false;
        }
    }

    handleResize = () => {

        this.viewportWidth = window.innerWidth;

    };

    disconnectedCallback() {

        window.removeEventListener(
            'resize',
            this.handleResize
        );

    }

    get hasDashboardData() {

        return Object.keys(this.dashboardData).length > 0;

    }

}

Important Things to Notice

  • connectedCallback() initializes the component.
  • hasLoadedData prevents unnecessary repeated Apex calls during reconnection of the same component instance.
  • The resize handler is declared as an arrow function so that the same function reference can be used for both adding and removing the listener.
  • disconnectedCallback() removes the listener.
  • finally ensures that the loading spinner is stopped whether the Apex call succeeds or fails.
Important: The hasLoadedData flag prevents repeated loading only within the same component instance. If Salesforce creates a completely new instance, the dashboard data will be fetched again. Since the Apex method is cacheable, Salesforce can use its client-side cache where applicable.

Step 3: HTML Template

Create salesforceDashboard.html.

<template>

    <lightning-card
        title="Salesforce Dashboard"
        icon-name="standard:dashboard">

        <div class="slds-p-around_medium">

            <template lwc:if={isLoading}>

                <lightning-spinner
                    alternative-text="Loading dashboard">
                </lightning-spinner>

            </template>

            <template lwc:elseif={errorMessage}>

                <div class="slds-text-color_error">
                    {errorMessage}
                </div>

            </template>

            <template lwc:elseif={hasDashboardData}>

                <div class="slds-grid slds-wrap">

                    <div class="slds-col
                                slds-size_1-of-1
                                slds-medium-size_1-of-3
                                slds-p-around_small">

                        <lightning-tile label="Total Accounts">
                            <p class="slds-text-heading_large">
                                {dashboardData.accounts}
                            </p>
                        </lightning-tile>

                    </div>

                    <div class="slds-col
                                slds-size_1-of-1
                                slds-medium-size_1-of-3
                                slds-p-around_small">

                        <lightning-tile label="Opportunities">
                            <p class="slds-text-heading_large">
                                {dashboardData.opportunities}
                            </p>
                        </lightning-tile>

                    </div>

                    <div class="slds-col
                                slds-size_1-of-1
                                slds-medium-size_1-of-3
                                slds-p-around_small">

                        <lightning-tile label="Open Cases">
                            <p class="slds-text-heading_large">
                                {dashboardData.cases}
                            </p>
                        </lightning-tile>

                    </div>

                </div>

                <div class="slds-m-top_medium">
                    <p>
                        Browser Width: {viewportWidth}px
                    </p>
                </div>

            </template>

            <template lwc:else>
                <p>No dashboard data available.</p>
            </template>

        </div>

    </lightning-card>

</template>

Step 4: Test the Component

Add the component to a Lightning App Page and test the following scenarios:

  1. Open the page and check whether dashboard data loads.
  2. Open the browser console and observe the lifecycle logs if you add them.
  3. Resize the browser window and check whether the width changes.
  4. Navigate away from the page and verify that the resize listener is removed.
  5. Navigate back and check that the component works correctly.

7. Common LWC Lifecycle Mistakes Developers Should Avoid

Mistake Better Approach
Calling Apex repeatedly in renderedCallback() Load data in connectedCallback() or use @wire.
Updating reactive fields on every render Use controlled initialization.
Adding event listeners without cleanup Remove them in disconnectedCallback().
Using getters for expensive calculations Keep getters lightweight.
Using @track everywhere Use it only when deep mutation tracking is needed.
Accessing DOM in constructor Use renderedCallback() when DOM access is required.
Assuming connectedCallback() runs only once Account for component reconnection.

8. Salesforce LWC Lifecycle – Interview Questions

Q1. What is the difference between connectedCallback() and renderedCallback()?

connectedCallback() executes when a component is inserted into the DOM. renderedCallback() executes after the component has rendered its DOM.

Q2. Can connectedCallback() execute multiple times?

Yes. If a component is removed and inserted into the DOM again, this hook can execute again.

Q3. Why should we avoid updating reactive properties inside renderedCallback()?

Because updating reactive properties can trigger another render, which can invoke renderedCallback() again and potentially create an infinite rendering loop.

Q4. What is the purpose of disconnectedCallback()?

It is used for cleanup activities such as removing event listeners, clearing timers, and unsubscribing from message channels.

Q5. Is @track mandatory for all properties in LWC?

No. Primitive properties and new object or array assignments are reactive by default. @track is useful for observing deep mutations in plain objects and arrays.

Q6. What is the purpose of a getter in LWC?

A getter calculates a value dynamically and allows us to keep presentation logic outside the HTML template.

Q7. Can we call Apex from connectedCallback()?

Yes. We can invoke an imperative Apex method from connectedCallback(). Alternatively, we can use @wire when reactive data provisioning is appropriate.

Q8. How do you prevent duplicate event listeners?

Use a stable handler reference, register it when the component connects, and remove that exact reference in disconnectedCallback().

Q9. What happens when a reactive property changes?

LWC schedules a rerender when a property used by the template changes. The framework efficiently updates the necessary DOM.

Q10. When should we use renderedCallback()?

Use it when an operation depends on the rendered DOM, such as initializing a third-party library or measuring an element.

Conclusion

Understanding the LWC lifecycle is not just about remembering six lifecycle hooks. It is about knowing when to execute a particular piece of code and how to manage resources efficiently.

In real Salesforce projects, a component may be added and removed multiple times, receive frequent data updates, and handle different user interactions.

A good LWC developer should focus on three things:

  1. Initialize only what is necessary.
  2. Let the framework handle reactivity and rendering.
  3. Always clean up resources when the component is removed.

These small practices make a big difference when building scalable, maintainable, and high-performance Salesforce applications.

Happy Coding!

Khumed Khatib
Salesforce Developer | Salesforce Technical Blogger
KhumedSFDC

Article Tags

Salesforce Lightning Web Components LWC Lifecycle Salesforce Development JavaScript LWC Interview Questions