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:
@api
@track
@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
After Clicking "Open Modal" and After Clicking "Confirm"
After Clicking "Cancel"
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
After Clicking "Load Demo User" and After Clicking "Clear Data"
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
- Design intentional APIs - Your component's public interface is a contract
- Prefer composition over configuration - Smaller components with focused APIs
- Consider consumer experience - Make your components intuitive to use
- 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
- When modifying object properties directly
- For array mutations (push, splice, etc.)
- 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
- Modern Reactivity:
- No
@track
decorator needed (default behavior in modern LWC) - UI automatically updates when object reference changes
- Immutable Update Pattern:
this.user = {
...this.user, // Copy existing properties
email: newValue // Override specific property
};
Output:
Initial State
After Clicking "Update Email"
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
- Legacy code maintenance
- When working with third-party libraries that mutate objects
- 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
- Two Syntax Options:
- Property binding (automatic)
- Function binding (manual control)
- Reactivity:
- Automatically refreshes when parameters change
- Uses
$
prefix for reactive variables (e.g.,'$recordId'
) - 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:
- Prefer
@wire
for read operations - Use imperative Apex for writes
- Always handle errors
- Debounce rapid parameter changes
- Cache frequently-used data
Next Steps:
- Implement
@wire
in your components - Explore Lightning Data Service adapters
- Consider custom wire adapters for complex scenarios
This powerful feature will significantly simplify your data layer in LWC!
(0) Comments