Understanding Decorators and Lifecycle Hooks in Salesforce LWC

Lightning Web Components (LWC) in Salesforce rely on decorators and lifecycle hooks to manage component behavior efficiently. These concepts are essential for controlling data flow, reacting to events, and optimizing performance.

In this blog, we'll explore:

  • What are Decorators?
  • Common Decorators in LWC (@api, @track, @wire)
  • Lifecycle Hooks in LWC (connectedCallback, renderedCallback, etc.)
  • Practical Examples with Code & Output

Let's dive in!

What are Decorators in LWC?


Decorators in LWC are JavaScript annotations that provide additional behavior to class properties and methods.

The three primary decorators in LWC are:

  1. @api
  2. @track
  3. @wire

1. @api (Public Property)


The @api decorator is a fundamental feature in Lightning Web Components that enables component communication and public exposure of properties and methods.

What is @api?

  • A decorator that marks properties/methods as public
  • Enables parent-to-child communication
  • Makes components reusable and configurable
  • Supports reactive properties (changes trigger re-render)

Key Characteristics


graph TD
    A[Parent Component] -->|Sets @api property| B[Child Component]
    B -->|Exposes @api method| A


Real-World Examples

1. Basic Property Passing

Child Component (childComponent.js)


import { LightningElement, api } from 'lwc';

export default class ChildComponent extends LightningElement {
    @api greeting = 'Hello'; // Public reactive property
}

Parent Component (parentComponent.html)


<template>
    <c-child-component greeting="Welcome!"></c-child-component>
</template>


2. Method Exposure (Child-to-Parent Communication)

Child Component (modal)

modal.html


<template>
    <div class={modalClass} role="dialog">
        <div class="slds-modal__container">
            <div class="slds-modal__header">
                <h2 class="slds-text-heading_medium">{headerText}</h2>
                <button class="slds-button slds-button_icon slds-modal__close" onclick={handleClose}>
                    <lightning-icon icon-name="utility:close" size="small"></lightning-icon>
                </button>
            </div>
            <div class="slds-modal__content slds-p-around_medium">
                <slot></slot>
            </div>
            <div class="slds-modal__footer">
                <button class="slds-button slds-button_brand" onclick={handleConfirm}>Confirm</button>
                <button class="slds-button slds-button_neutral" onclick={handleClose}>Cancel</button>
            </div>
        </div>
        <div class="slds-backdrop"></div>
    </div>
</template>

modal.js


import { LightningElement, api } from 'lwc';

export default class Modal extends LightningElement {
    @api headerText = 'Default Header';
    isVisible = false;

    get modalClass() {
        return this.isVisible ? 'slds-modal slds-fade-in-open' : 'slds-modal';
    }

    @api
    show() {
        this.isVisible = true;
    }

    @api
    hide() {
        this.isVisible = false;
    }

    handleClose() {
        this.hide();
        this.dispatchEvent(new CustomEvent('close'));
    }

    handleConfirm() {
        this.dispatchEvent(new CustomEvent('confirm'));
    }
}

Parent Component (parent)

parent.html


<template>
    <div class="slds-p-around_large">
        <lightning-button 
            label="Open Modal" 
            onclick={openModal}
            variant="brand"
            class="slds-m-bottom_medium">
        </lightning-button>

        <p class="slds-m-top_medium">Last Action: {lastAction}</p>

        <c-modal 
            header-text="Confirmation Required"
            onclose={handleModalClose}
            onconfirm={handleModalConfirm}>
            <p>Are you sure you want to proceed with this action?</p>
        </c-modal>
    </div>
</template>

parent.js


import { LightningElement } from 'lwc';

export default class Parent extends LightningElement {
    lastAction = 'None';

    openModal() {
        this.template.querySelector('c-modal').show();
        this.lastAction = 'Modal opened';
    }

    handleModalClose() {
        this.lastAction = 'Modal closed';
    }

    handleModalConfirm() {
        this.lastAction = 'User confirmed action';
        this.template.querySelector('c-modal').hide();
    }
}

Output:

Initial State

Child-to-Parent Communication


After Clicking "Open Modal" and After Clicking "Confirm"

Child-to-Parent Communication


After Clicking "Cancel"

Child-to-Parent Communication



3. Complex Object Passing

Child Component (profileViewer)

profileViewer.js


import { LightningElement, api } from 'lwc';

export default class ProfileViewer extends LightningElement {
    @api user = {
        name: '',
        email: '',
        phone: ''
    };
}

profileViewer.html


<template>
    <div class="profile-container slds-p-around_medium">
        <div class="slds-card">
            <div class="slds-card__header slds-grid">
                <h2 class="slds-text-heading_medium">User Profile</h2>
            </div>
            <div class="slds-card__body">
                <template if:true={user.name}>
                    <div class="slds-m-bottom_small">
                        <span class="slds-text-title">Name:</span>
                        <span class="slds-text-body_regular">{user.name}</span>
                    </div>
                    <div class="slds-m-bottom_small">
                        <span class="slds-text-title">Email:</span>
                        <span class="slds-text-body_regular">{user.email}</span>
                    </div>
                    <div class="slds-m-bottom_small">
                        <span class="slds-text-title">Phone:</span>
                        <span class="slds-text-body_regular">{user.phone}</span>
                    </div>
                </template>
                <template if:false={user.name}>
                    <p>No user data available</p>
                </template>
            </div>
        </div>
    </div>
</template>

profileViewer.css


.profile-container {
    max-width: 400px;
    margin: 0 auto;
}

Parent Component (parent)

parent.js


import { LightningElement } from 'lwc';

export default class Parent extends LightningElement {
    currentUser = {};

    loadDemoUser() {
        this.currentUser = {
            name: 'Saurabh Samir',
            email: 'ssamir@learnfrenzy.com',
            phone: '(415) 555-0198',
            title: 'Senior Developer',
            department: 'Engineering'
        };
        console.log('User data loaded:', this.currentUser);
    }

    clearUserData() {
        this.currentUser = {};
        console.log('User data cleared');
    }
}

parent.html


<template>
    <div class="slds-p-around_large">
        <lightning-button label="Load Demo User" onclick={loadDemoUser} variant="brand" class="slds-m-bottom_medium">
        </lightning-button>
        
        <lightning-button label="Clear Data" onclick={clearUserData} variant="destructive" class="slds-m-bottom_medium">
        </lightning-button>
        
        <c-profile-viewer user={currentUser}></c-profile-viewer>
    </div>
</template>

Output:

Initial State

Complex Object Passing


After Clicking "Load Demo User" and After Clicking "Clear Data"

Complex Object Passing



Common Pitfalls

Pitfall Solution
Mutating @api property directly Use getter/setter
Overly complex API Break into smaller components
Missing reactivity Ensure parent updates the reference
Method naming conflicts Use explicit names (showModal vs show)


Performance Considerations

1. Avoid Frequent Updates


// Anti-pattern
setInterval(() => {
    this.template.querySelector('c-child').counter++;
}, 100);

2. Debounce Inputs


@api 
set searchTerm(value) {
    this._searchTerm = value;
    this.debouncedSearch();
}


Final Recommendations

  1. Design intentional APIs - Your component's public interface is a contract
  2. Prefer composition over configuration - Smaller components with focused APIs
  3. Consider consumer experience - Make your components intuitive to use
  4. Document thoroughly - Include examples and edge cases

Example Component Template:


/**
 * @name c-data-grid
 * @description Displays data in a customizable grid
 * @example
 * 
 */
export default class DataGrid extends LightningElement {
    @api columns = [];
    @api data = [];
    @api onrowclick = () => {};
}

The @api decorator is your primary tool for creating reusable, configurable components in LWC. By following these patterns and practices, you'll build components that are both powerful and maintainable.



2. @track (Reactive Property - Limited Use)


@track makes a property reactive, meaning changes to its value trigger component re- rendering.


What is @track?

@track is a decorator in Lightning Web Components that:

  • Marks a property as reactive (UI updates when it changes)
  • Was essential in early LWC for object/array reactivity
  • Now mostly implicit (modern LWC makes all fields reactive by default)
  • Still useful for specific edge cases


Evolution of Reactivity in LWC


timeline
    title Reactivity in LWC Timeline
    2019 : @track required for objects/arrays
    2020 : Primitive properties always reactive
    2021+ : @track rarely needed (implicit reactivity)

When You Still Need @track

  1. When modifying object properties directly
  2. For array mutations (push, splice, etc.)
  3. When working with complex nested objects

Real-World Examples

1. Basic Object Reactivity (Modern Approach)

JavaScript (userProfile.js)


import { LightningElement } from 'lwc';

export default class UserProfile extends LightningElement {
    // Modern LWC: Reactive by default
    user = {
        name: 'Saurabh Samir',
        email: 'saurabh@learnfrenzy.com'
    };

    updateEmail() {
        // This will trigger UI update automatically
        this.user = {
            ...this.user, // Spread existing properties
            email: 'saurabh.samir@learnfrenzy.com' // Update email
        };
    }
}

HTML (userProfile.html)


<template>
  <lightning-card title="User Profile" class="slds-p-around_medium">
    <div class="slds-card__body">
      <!-- Name Field -->
      <div class="slds-form-element slds-m-bottom_medium">
        <div class="slds-grid slds-wrap slds-gutters">
          <div class="slds-col slds-size_2-of-12">
            <label class="slds-form-element__label">Name:</label>
          </div>
          <div class="slds-col slds-size_10-of-12">
            <div class="slds-form-element__control">
              <span class="slds-text-body_regular">{user.name}</span>
            </div>
          </div>
        </div>
      </div>

      <!-- Email Field -->
      <div class="slds-form-element slds-m-bottom_medium">
        <div class="slds-grid slds-wrap slds-gutters">
          <div class="slds-col slds-size_2-of-12">
            <label class="slds-form-element__label">Email:</label>
          </div>
          <div class="slds-col slds-size_10-of-12">
            <div class="slds-form-element__control">
              <span class="slds-text-body_regular">{user.email}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="slds-card__footer">
      <lightning-button label="Update Email" onclick={updateEmail} variant="brand">
      </lightning-button>
    </div>
  </lightning-card>
</template>

Key Features

  1. Modern Reactivity:
    • No @track decorator needed (default behavior in modern LWC)
    • UI automatically updates when object reference changes
  2. Immutable Update Pattern:
  3. 
    this.user = {
        ...this.user, // Copy existing properties
        email: newValue // Override specific property
    };
    

Output:

Initial State

Basic Object Reactivity (Modern Approach)


After Clicking "Update Email"

Basic Object Reactivity (Modern Approach)



2. When @track is Still Needed


import { LightningElement, track } from 'lwc';

export default class ShoppingCart extends LightningElement {
    // Needed for direct property assignment
    @track cart = {
        items: [],
        total: 0
    };

    addItem(item) {
        // Without @track, this wouldn't trigger UI update
        this.cart.items.push(item);
        this.cart.total += item.price;
        
        // Required to notify framework of change
        this.cart = {...this.cart};
    }
}

3. Array Manipulation Example


import { LightningElement } from 'lwc';

export default class TaskList extends LightningElement {
    // Modern approach (no @track needed)
    tasks = [];

    addTask() {
        // This works because we're reassigning
        this.tasks = [...this.tasks, newTask];
    }

    removeTask(index) {
        // Create new array without the item
        this.tasks = this.tasks.filter((_, i) => i !== index);
    }
}


Key Patterns & Best Practices

1. Immutable Updates (Recommended)


// Instead of:
this.someObject.property = newValue;

// Do:
this.someObject = {
    ...this.someObject,
    property: newValue
};

2. Array Operations

Operation Modern Approach Legacy (@track) Approach
Add item [...array, newItem] array.push(newItem)
Remove item filter() array.splice(index, 1)
Update item map() array[index] = newValue


3. Nested Object Updates


// Deep clone pattern
updateNestedProperty() {
    this.complexData = {
        ...this.complexData,
        level1: {
            ...this.complexData.level1,
            level2: newValue
        }
    };
}


Performance Considerations

  • Immutable updates create new objects (memory overhead)
  • @track with direct mutations can be more memory efficient
  • For large datasets, consider specialized state management

Common Pitfalls & Solutions

Problem Solution
UI not updating Use immutable pattern or add @track
Performance issues For large arrays, use specialized libraries
Deep nesting complexity Consider flattening state structure


When to Use @track Today

  1. Legacy code maintenance
  2. When working with third-party libraries that mutate objects
  3. Specific performance optimization cases

Final Recommendation

  • Default to modern reactivity (no @track)
  • Use immutable patterns for state updates
  • Only add @track when you encounter specific reactivity issues
  • For complex state, consider stores (like lightning/uiRecordApi)

// Modern best practice component example
import { LightningElement } from 'lwc';

export default class BestPracticeExample extends LightningElement {
    // All properties are reactive
    state = {
        user: {
            name: 'Alex',
            preferences: {}
        },
        items: []
    };

    updateUser() {
        // Proper immutable update
        this.state = {
            ...this.state,
            user: {
                ...this.state.user,
                name: 'Alexander'
            }
        };
    }
}

The @track decorator has largely become optional in modern LWC development, but understanding its behavior remains important for debugging edge cases and working with legacy codebases.



3. @wire (Fetch Data from Salesforce)


The @wire decorator is one of the most powerful features in Lightning Web Components (LWC), enabling seamless data access from Salesforce. Let's break it down comprehensively.

What is @wire?

@wire is a decorator that:

  • Fetches data from Salesforce and automatically updates the UI when data changes.
  • Supports Apex methods and Lightning Data Service (LDS)
  • Uses reactive data binding, meaning the UI updates automatically when the underlying data changes.
  • More efficient than imperative Apex calls for retrieving frequently updated data.

Example using @wire:


import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountList extends LightningElement {
    @wire(getAccounts) accounts;
}

Comparing @wire with Imperative Apex Calls

Imperative Apex Call Example:


import { LightningElement, track } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountList extends LightningElement {
    @track accounts;

    connectedCallback() {
        getAccounts()
            .then(result => {
                this.accounts = result;
            })
            .catch(error => {
                console.error('Error fetching accounts', error);
            });
    }
}

How @wire Works: Key Concepts

  1. Two Syntax Options:
    • Property binding (automatic)
    • Function binding (manual control)
  2. Reactivity:
    • Automatically refreshes when parameters change
    • Uses $ prefix for reactive variables (e.g., '$recordId')
  3. Data Flow:

graph LR
A[Component] -- @wire --> B[Apex/LDS]
B -- Data/Error --> A


Real-World Examples

1. Basic Apex Method Call (Property Syntax)


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

export default class ContactList extends LightningElement {
    @wire(getContacts) 
    contacts; // Automatic property binding

    // Template can access:
    // contacts.data (records)
    // contacts.error (if any)
}

HTML Usage:


<template>
    <template if:true={contacts.data}>
        <template for:each={contacts.data} for:item="contact">
            <p key={contact.Id}>{contact.Name}</p>
        </template>
    </template>
    <template if:true={contacts.error}>
        <p class="error">Error loading contacts</p>
    </template>
</template>

2. Parameterized Apex Call (Function Syntax)


import { LightningElement, wire, api } from 'lwc';
import getOpportunities from '@salesforce/apex/OpportunityController.getByAccount';

export default class OppList extends LightningElement {
    @api recordId; // Parent component passes account ID

    @wire(getOpportunities, { accountId: '$recordId' })
    wiredOpportunities({ data, error }) {
        if (data) this.processData(data);
        if (error) this.handleError(error);
    }

    processData(data) {
        // Transform data as needed
    }
}

3. Lightning Data Service (LDS) - Get Record


import { LightningElement, wire, api } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';

export default class AccountViewer extends LightningElement {
    @api recordId;

    // Get standard fields
    @wire(getRecord, { 
        recordId: '$recordId', 
        fields: [NAME_FIELD] 
    })
    account;
}

HTML Usage:


<template>
    <template if:true={account.data}>
        <h2>{account.data.fields.Name.value}</h2>
    </template>
</template>

4. Custom Wire Adapter (Advanced)


// customWireAdapter.js
import { LightningElement, wire } from 'lwc';
import { CurrentPageReference } from 'lightning/navigation';

export default class UrlParamsReader extends LightningElement {
    @wire(CurrentPageReference)
    pageRef;

    get recordId() {
        return this.pageRef?.state?.c__recordId;
    }
}


Key Patterns & Best Practices

1. Reactive Parameters

Prefix variables with $ to auto-refresh when values change:


@wire(getContacts, { searchTerm: '$searchKey' })
contacts;

2. Error Handling


@wire(getData)
wiredData({ error, data }) {
    if (error) {
        this.error = error.body?.message;
        notifyUser('Error fetching data');
    }
    if (data) this.data = data;
}

3. Combining Multiple Wires


export default class CombinedData extends LightningElement {
    @wire(getContacts) contacts;
    @wire(getAccounts) accounts;

    get allDataLoaded() {
        return this.contacts.data && this.accounts.data;
    }
}

4. Performance Optimization


// Debounce rapid parameter changes
@wire(getContacts, { searchTerm: '$debouncedSearchTerm' })
contacts;

get debouncedSearchTerm() {
    return this._debounce(this.searchTerm, 300);
}


Common Pitfalls & Solutions

Pitfall Solution
"Cannot read property 'data' of undefined" Always check if:true={wire.data} in template
Infinite loops from reactive params Use immutable data patterns
Too many server calls Implement debouncing
Large data payloads Use pagination (LIMIT/OFFSET in SOQL)


Advanced Use Cases

1. Dynamic Field Selection


// Dynamically wire fields based on user selection
get wiredFields() {
    return [FIELD1, FIELD2]; // Change based on input
}

@wire(getRecord, { 
    recordId: '$recordId',
    fields: '$wiredFields'
})
record;

2. Client-Side Caching


// Cache wire results in a store
import { store } from 'c/store';

@wire(getContacts)
wiredContacts({ data }) {
    if (data) store.set('contacts', data);
}


Comparison: @wire vs Imperative Apex

Feature @wire Imperative Apex
Data Fetching Automatic, reactive Manual, only runs when called
Performance Optimized for reactivity Requires explicit refresh
Usage Best for frequently updated data Best for on-demand data fetching

Use @wire when you want automatic updates, and use imperative Apex when you need full control over when data is fetched.

Final Thoughts

The @wire decorator:

  • Reduces boilerplate for data access
  • Handles reactivity automatically
  • Integrates seamlessly with Salesforce data

Best Practices:

  1. Prefer @wire for read operations
  2. Use imperative Apex for writes
  3. Always handle errors
  4. Debounce rapid parameter changes
  5. Cache frequently-used data

Next Steps:

  1. Implement @wire in your components
  2. Explore Lightning Data Service adapters
  3. Consider custom wire adapters for complex scenarios

This powerful feature will significantly simplify your data layer in LWC!

Share This Post:

About The Author

Hey, my name is Saurabh Samir, and I am a Salesforce Developer with a passion for helping you elevate your knowledge in Salesforce, Lightning Web Components (LWC), Salesforce triggers, and Apex. I aim to simplify complex concepts and share valuable insights to enhance your Salesforce journey. Do comment below if you have any questions or feedback—I'd love to hear from you!