---
title: "The LWC lifecycle hooks, and the renderedCallback trap"
url: "https://trailassembly.com/lwc-lifecycle-hooks/"
topic: "LWC and Front-End"
published: "2026-09-18T10:00:33+00:00"
updated: "2026-10-06T13:58:03+00:00"
---

# The LWC lifecycle hooks, and the renderedCallback trap

**The short answer:** there are five hooks worth knowing. `constructor` sets field defaults and nothing else, `connectedCallback` is where subscriptions and initial data fetches belong, `renderedCallback` is the only place you may touch rendered DOM, `disconnectedCallback` undoes whatever `connectedCallback` set up, and `errorCallback` catches failures thrown by components below you. The one that bites everybody is `renderedCallback`, because it runs after every render and changing [reactive](https://trailassembly.com/flow-reactive-screen-components/) state inside it schedules another render.

## The order they run in

A component is constructed, then connected, then rendered. Parents construct before children, but children finish rendering before parents. So a parent’s `renderedCallback` runs after all of its children have rendered, which is occasionally useful and frequently surprising.

## constructor

Call `super()` first. That is not a style preference. The framework will throw if you do not.

```apex
constructor() {
    super();
    this.rows = [];
    this.isLoading = true;
}
```

The element exists at this point but has no attributes and no children. `this.template.querySelector()` returns nothing. Public properties set by a parent have not arrived yet, so reading `this.recordId` here gives you `undefined` every time. Do not add attributes to the host element and do not call anything asynchronous.

Most components do not need a constructor at all. Field initialisers cover the same ground with less ceremony.

## connectedCallback

The element has been inserted into the document. Public properties are set, so this is the earliest point where `recordId` is reliable. Use it for:

- Subscribing to a [message channel](https://trailassembly.com/lwc-component-communication/) or a platform event

- Firing an imperative Apex call for data you need immediately

- Registering an event listener on `window` or `document`

- Starting a timer

It can run more than once on the same instance. If the element is removed from the DOM and reinserted, you get a second `connectedCallback`, and a subscription created without a matching teardown is now duplicated. Guard anything that must happen once, or make the teardown symmetrical.

You still cannot query the template here. Nothing has rendered.

## renderedCallback and the loop

This fires after every single render pass, not once. Any reactive property you change inside it marks the component dirty, the framework renders again, and `renderedCallback` fires again. That is an infinite loop, and it presents as a frozen tab rather than an error in the console.

```apex
renderedCallback() {
    if (this.hasInitialised) {
        return;
    }
    this.hasInitialised = true;
    loadScript(this, CHART_JS).then(() => this.drawChart());
}
```

The boolean guard is the standard fix, and `hasInitialised` should be a plain field, not decorated with `@track`, so that setting it does not itself trigger a render.

Legitimate work in here: initialising a third-party library against a container element, setting focus on first paint, measuring an element’s size. Everything else belongs somewhere earlier. If you find yourself reading values out of the DOM to decide what to render next, the state should have been in JavaScript to begin with.

## disconnectedCallback

The mirror of `connectedCallback`. Unsubscribe from message channels, clear intervals, remove window listeners, disconnect observers. On a record page where users click from one record to the next, a leaked subscription accumulates silently and the symptom appears half an hour later as duplicate handling of a single event.

```apex
disconnectedCallback() {
    unsubscribe(this.subscription);
    this.subscription = null;
    window.removeEventListener('resize', this.handleResize);
}
```

Note that the listener must be the same function reference you added. An inline arrow function passed to `addEventListener` cannot be removed.

## errorCallback

Declare `errorCallback(error, stack)` and the component becomes a boundary for its descendants. An error thrown in a child’s lifecycle hook or render is caught here instead of breaking the page.

Its scope is narrower than people expect. It does not catch errors in the component’s own hooks, in event handlers, or inside promise chains. A rejected Apex promise will not reach it. Treat it as a safety net around a subtree you do not control, and handle your own failures with `try/catch` and `.catch()` where they occur.

## The sixth one

`render()` is not really a lifecycle hook, it is an override that returns a template. Use it to pick between two imported HTML files when the difference is structural rather than conditional. If you are choosing between two markup blocks based on state, `lwc:if` is clearer and cheaper.

## Frequently asked

**Why is recordId undefined in my constructor?** Public properties are assigned after construction. Read it in `connectedCallback`, or use a setter if it can change while the component is alive.

**Can I call Apex from connectedCallback?** Yes, imperatively. A `@wire` fires on its own schedule and does not need a hook at all.

**How do I run code once after the first render only?** A boolean field set inside `renderedCallback`. There is no built-in first-render hook.

**Does disconnectedCallback always run?** When the component is removed from the DOM, yes. A full page reload or a browser tab closing does not give you one.

---

Source: https://trailassembly.com/lwc-lifecycle-hooks/
Trail Assembly — independent Salesforce write-ups.
