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
@apivalues in the constructor. The parent hasn't passed them yet. - Use
connectedCallbackto set up subscriptions and run the first data load. renderedCallbackfires 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
disconnectedCallbackto avoid memory leaks. errorCallbackon a parent catches errors from its child components, never from itself.
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
@apivalues. Runs parent to child. - 2. connectedCallback: Runs when the component is inserted into the DOM.
@apivalues 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.
π 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
π 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.
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
}
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
}
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
}
}
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
}
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
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.
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().
- Always Clean Up: Every listener, timer, or subscription you add in
connectedCallback()needs a matching cleanup indisconnectedCallback(). Otherwise, old handlers keep running and memory leaks build up. - Prefer Template Directives: To show or hide markup, use
lwc:if,lwc:elseif, andlwc:else. Overriderender()only when you need to switch between completely different HTML template files.
π Core Q&A & Scenarios
renderedCallback (it runs again and again), DOM access in connectedCallback (it's too early), and no cleanup in disconnectedCallback.
Detailed breakdown:
- Mistake 1: Reading
@apivalues in the constructor. They aren't set yet. Move that code toconnectedCallback. - Mistake 2: One-time setup in
renderedCallbackwithout 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
connectedCallbackand never removing it indisconnectedCallback. 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 indisconnectedCallback. - Mistake 5: Expecting a child's DOM to be ready in the parent's
connectedCallback. The children haven't rendered yet.
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.
@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.
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.
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.
Comments
Questions, corrections and your own experience are all welcome.
Post a Comment