Salesforce LWC Lifecycle Hooks Explained

Introduction

Lightning Web Components (LWC) is Salesforce’s modern framework for building custom user interfaces in Salesforce applications. Understanding the component lifecycle is crucial for efficient component development, optimizing performance, and implementing complex functionalities.

What are Lifecycle Hooks?

Lifecycle hooks are special methods that get executed at specific points during a component’s existence. They allow developers to run custom code at key moments such as when a component is created, rendered, updated, or removed from the DOM. By leveraging these hooks strategically, you can control component behavior throughout its lifecycle.

The LWC Component Lifecycle

A Lightning Web Component goes through the following phases:

  1. Creation: The component is constructed and initialized
  2. Rendering: The component’s HTML template is rendered to the DOM
  3. DOM Insertion: The component is inserted into the DOM
  4. Updates: The component’s state changes, causing re-renders
  5. Removal: The component is removed from the DOM

Let’s explore each lifecycle hook in detail.

Constructor

When it runs: At component creation, before any other lifecycle hook.

Use cases:

  • Initialize component properties
  • Set initial state
  • Bind event methods

Do not:

  • Access DOM elements
  • Call external services
  • Access child components

Example:

import { LightningElement } from 'lwc';

export default class LifecycleDemo extends LightningElement {
    constructor() {
        super(); // Always call super() first
        
        // Initialize properties
        this.counter = 0;
        this.initialized = true;
        
        // Bind methods to maintain 'this' context
        this.handleClick = this.handleClick.bind(this);
        
        console.log('Constructor executed');
    }
}

connectedCallback

When it runs: When a component is inserted into the DOM.

Use cases:

  • Fetch data from an Apex controller
  • Subscribe to events
  • Set up timers or intervals
  • Initialize third-party libraries

Example:

import { LightningElement } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';

export default class LifecycleDemo extends LightningElement {
    contacts;
    error;
    
    connectedCallback() {
        console.log('Component connected to DOM');
        
        // Fetch data when component is inserted into DOM
        getContacts()
            .then(result => {
                this.contacts = result;
                this.error = undefined;
            })
            .catch(error => {
                this.error = error;
                this.contacts = undefined;
            });
            
        // Set up interval
        this.intervalId = setInterval(() => {
            this.counter++;
        }, 1000);
        
        // Subscribe to platform events
        this.subscription = subscribe(
            this.channelName,
            -1,
            this.handleEvent.bind(this)
        );
    }
}

renderedCallback

When it runs: After every render of the component. It runs after the first render and after every re-render caused by state changes.

Use cases:

  • Access or manipulate DOM elements
  • Integrate with third-party libraries that need DOM access
  • Perform calculations based on rendered component dimensions
  • Adjust component layout after rendering

Example:

import { LightningElement } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import chartjs from '@salesforce/resourceUrl/chartjs';

export default class ChartComponent extends LightningElement {
    chart;
    chartInitialized = false;
    
    renderedCallback() {
        console.log('Component rendered/re-rendered');
        
        // Only initialize the chart library once
        if (this.chartInitialized) {
            return;
        }
        this.chartInitialized = true;
        
        // Load the ChartJS library
        loadScript(this, chartjs)
            .then(() => {
                // Get the canvas element
                const canvas = this.template.querySelector('canvas');
                
                // Initialize chart
                this.chart = new Chart(canvas, {
                    type: 'bar',
                    data: {
                        labels: ['Red', 'Blue', 'Yellow'],
                        datasets: [{
                            label: 'Colors',
                            data: [12, 19, 3],
                            backgroundColor: [
                                'rgba(255, 99, 132, 0.2)',
                                'rgba(54, 162, 235, 0.2)',
                                'rgba(255, 206, 86, 0.2)'
                            ],
                            borderColor: [
                                'rgba(255, 99, 132, 1)',
                                'rgba(54, 162, 235, 1)',
                                'rgba(255, 206, 86, 1)'
                            ],
                            borderWidth: 1
                        }]
                    }
                });
            })
            .catch(error => {
                console.error('Error loading ChartJS', error);
            });
    }
}

disconnectedCallback

When it runs: When a component is removed from the DOM.

Use cases:

  • Clean up resources
  • Cancel pending operations
  • Unsubscribe from events
  • Clear timers or intervals
  • Release memory

Example:

import { LightningElement } from 'lwc';
import { unsubscribe } from 'lightning/empApi';

export default class LifecycleDemo extends LightningElement {
    subscription;
    intervalId;
    
    connectedCallback() {
        // Set up interval
        this.intervalId = setInterval(() => {
            console.log('Interval running');
        }, 1000);
        
        // Subscribe to platform events
        this.subscription = subscribe(
            this.channelName,
            -1,
            this.handleEvent.bind(this)
        );
    }
    
    disconnectedCallback() {
        console.log('Component removed from DOM');
        
        // Clear interval
        if (this.intervalId) {
            clearInterval(this.intervalId);
        }
        
        // Unsubscribe from events
        if (this.subscription) {
            unsubscribe(this.subscription);
        }
        
        // Clean up any other resources
        if (this.chart) {
            this.chart.destroy();
        }
    }
}

errorCallback

errorCallback(error, stack)

  • When it runs: Only when an unhandled error occurs in a child component
  • Purpose: Provides a way for parent components to handle errors from their child components
  • Parameters:
    • error: The JavaScript error object
    • stack: The stack trace string

The errorCallback does not catch errors that occur:

  • In the component itself where the errorCallback is defined
  • In event handlers of the component
  • In the component’s own lifecycle hooks
  • In the component’s render() method

Example:

<!-- parentComponent.html -->
<template>
    <div class="parent-container">
        <h2>Parent Component</h2>
        
        <!-- Conditionally display error message -->
        <template if:true={showError}>
            <div class="error-message">
                <lightning-icon icon-name="utility:error" alternative-text="Error" variant="error"></lightning-icon>
                <span>{errorMessage}</span>
            </div>
        </template>
        
        <!-- Child component that might throw an error -->
        <c-error-prone-child></c-error-prone-child>
        
        <div class="container">
            <p>The parent component can catch and handle errors from the child component above.</p>
        </div>
    </div>
</template>
// parentComponent.js
import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {
    // errorCallback lifecycle hook
    // This runs when a child component throws an error
    errorCallback(error, stack) {
        // Handle error from child component
        console.error('Error in child component:', error.message);
        console.log('Stack trace:', stack);
        
        // Implement custom error handling
        // For example, display a friendly message to the user
        this.errorMessage = 'Something went wrong in a child component. Please try again later.';
        
        // You could also report the error to a logging service
        // this.logErrorToService(error, stack);
        
        // Return true to indicate that you've handled the error
        return true;
    }
    
    // Example method to log errors to a service
    logErrorToService(error, stack) {
        // Implementation for error logging
    }
}
/* parentComponent.css */
.parent-container {
    padding: 1rem;
    background-color: #f3f3f3;
    border-radius: 4px;
}

.error-message {
    background-color: #ffdde1;
    color: #c23934;
    padding: 0.5rem;
    border-radius: 4px;
    margin: 1rem 0;
    display: flex;
    align-items: center;
}

.error-message lightning-icon {
    margin-right: 0.5rem;
}

.container {
    margin-top: 1rem;
}

Child Component

// errorProneChild.js
import { LightningElement, api } from 'lwc';

export default class ErrorProneChild extends LightningElement {
    @api 
    get throwError() {
        return this._throwError;
    }
    set throwError(value) {
        this._throwError = value;
        if (value === true) {
            this.causeError();
        }
    }
    
    _throwError = false;
    
    connectedCallback() {
        // This will intentionally cause an error after the component is connected
        // In a real scenario, errors might happen due to data issues, unexpected state, etc.
        setTimeout(() => {
            this.causeError();
        }, 2000);
    }
    
    causeError() {
        // This will cause an error that will be caught by the parent's errorCallback
        const nonExistentObject = undefined;
        try {
            // This will throw a TypeError
            nonExistentObject.someProperty = 'This will cause an error';
        } catch (error) {
            // Throwing the error from here will trigger the parent's errorCallback
            throw new Error('Child component failed: ' + error.message);
        }
    }
    
    handleClick() {
        // Another way to trigger an error
        this.causeError();
    }
}
<!-- errorProneChild.html -->
<template>
    <div class="child-container">
        <h3>Error-Prone Child Component</h3>
        <p>This component will throw an error shortly after loading, which the parent will catch.</p>
        <lightning-button 
            label="Force Error" 
            variant="destructive" 
            onclick={handleClick}>
        </lightning-button>
    </div>
</template>
/* errorProneChild.css */
.child-container {
    padding: 1rem;
    background-color: white;
    border: 1px solid #d8dde6;
    border-radius: 4px;
    margin-top: 1rem;
}

render()

When it runs: Before each time a component renders or re-renders.

Use cases:

  • Conditionally specify which template to render
  • Implement dynamic templates based on state
  • Swap templates based on device type or form factor

Example:

import { LightningElement } from 'lwc';
import desktopTemplate from './desktopView.html';
import mobileTemplate from './mobileView.html';
import tabletTemplate from './tabletView.html';

export default class ResponsiveComponent extends LightningElement {
    formFactor = 'DESKTOP';
    
    // Determine device type on component initialization
    connectedCallback() {
        // This is simplified - you'd use more robust detection in a real app
        const width = window.innerWidth;
        if (width < 768) {
            this.formFactor = 'PHONE';
        } else if (width < 1024) {
            this.formFactor = 'TABLET';
        } else {
            this.formFactor = 'DESKTOP';
        }
    }
    
    // Dynamically return the appropriate template
    render() {
        console.log('Render method called, form factor:', this.formFactor);
        
        switch(this.formFactor) {
            case 'PHONE':
                return mobileTemplate;
            case 'TABLET':
                return tabletTemplate;
            default:
                return desktopTemplate;
        }
    }
}

Lifecycle Flow and Best Practices

Order of Execution

For a typical component, lifecycle hooks are executed in this order:

  1. constructor()
  2. render() (initial)
  3. connectedCallback()
  4. renderedCallback() (first render)

When a component’s state changes:

  1. render()
  2. renderedCallback()

When a component is removed:

  1. disconnectedCallback()

Best Practices

  1. Keep the constructor lightweight
    • Avoid complex operations
    • Only initialize properties
  2. Fetch data in connectedCallback
    • Not in constructor
    • Not in renderedCallback (would create infinite loops)
  3. Interact with DOM in renderedCallback
    • Never in constructor or connectedCallback
    • Add guards to prevent repeated executions when not needed
  4. Use errorCallback for robust error handling
    • Always provide user-friendly error messages
    • Log detailed errors for debugging
  5. Always clean up in disconnectedCallback
    • Cancel subscriptions
    • Clear timers
    • Release resources

Troubleshooting Common Issues

Infinite Rendering Loops

A common issue is creating infinite rendering loops. This happens when you modify reactive properties in renderedCallback() without proper guards.

Problem:

renderedCallback() {
    // This creates an infinite loop!
    this.counter++;
}

Solution:

renderedCallback() {
    if (this.firstRender) {
        this.firstRender = false;
        this.counter++;
    }
}

Accessing DOM Elements Too Early

Problem:

connectedCallback() {
    // This will fail - DOM isn't ready yet
    const div = this.template.querySelector('div');
    div.focus();
}

Solution:

renderedCallback() {
    // Now the DOM is ready
    const div = this.template.querySelector('div');
    div.focus();
}

Advanced Patterns with Lifecycle Hooks

Component Communication with Lifecycle

// Parent component
import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {
    childReady = false;
    
    handleChildReady() {
        this.childReady = true;
        console.log('Child component is ready');
    }
}
// Child component
import { LightningElement } from 'lwc';

export default class ChildComponent extends LightningElement {
    connectedCallback() {
        // Notify parent that child is ready
        setTimeout(() => {
            const readyEvent = new CustomEvent('childready');
            this.dispatchEvent(readyEvent);
        }, 0);
    }
}

Conditional Rendering and Lifecycle

import { LightningElement, track } from 'lwc';

export default class ConditionalComponent extends LightningElement {
    @track showComponent = false;
    
    toggleComponent() {
        this.showComponent = !this.showComponent;
    }
    
    // This will run whenever component is shown or hidden
    renderedCallback() {
        console.log('Component render state changed:', this.showComponent ? 'showing' : 'hidden');
    }
}

Conclusion

Understanding and effectively using Lightning Web Component lifecycle hooks is essential for building robust, performant Salesforce applications. By applying the right hook at the right time, you can create components that initialize properly, interact seamlessly with external systems, clean up after themselves, and handle errors gracefully.

Remember the key principles:

  • Use the constructor for basic initialization
  • Use connectedCallback for setup and data fetching
  • Use renderedCallback for DOM interactions
  • Use disconnectedCallback for cleanup
  • Use errorCallback for error handling
  • Use render() for dynamic templates

Must Read

Amit Singh
Amit Singh

Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion

Articles: 299

Newsletter Updates

Enter your email address below and subscribe to our newsletter

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading