Skip to content

v6.0.0

Compare
Choose a tag to compare
@salesforce-nucleus salesforce-nucleus released this 17 Jan 20:08
5557079

Note

These release notes are for the Lightning Web Components framework in open source only. If you use Lightning Base Components, Lightning Web Security, or other Salesforce products on the Lightning platform, see the Summer '24 release notes.

LWC v6.0.0 contains breaking changes. Please read carefully below if you are upgrading from v5.

If you are upgrading from v4, please upgrade to v5 first.

Note

LWC v6 corresponds to Salesforce release Summer '24 (API version 61).

Summary of new features

Summary of breaking change

Breaking changes

Native custom element lifecycle

Note

This change currently only applies to consumers of the LWC open-source project outside of the Salesforce Lightning platform.

Previous versions of LWC used a synthetic (polyfill) system to call the connectedCallback and disconnectedCallback component lifecycle hooks. This was originally designed for legacy browsers such as Internet Explorer 11. Such browsers are no longer supported, and LWC v6 uses the native browser APIs instead.

Important

Native vs synthetic custom element lifecycle is not the same as native vs synthetic shadow DOM. They are two entirely separate concepts.

This change may affect the firing of connectedCallback and disconnectedCallback hooks in your components, since the previous implementation did not perfectly align with the browser standard. Most commonly, this will occur in unit tests, or when manually creating components using the lwc.createElement() API. The new behavior also fixes common memory leaks caused by disconnectedCallback not firing consistently.

Native custom element lifecycle also unlocks support for Form-Associated Custom Elements (FACE) and Element Internals.

Changes you may see to connectedCallback and disconnectedCallback behavior:

New behavior when disconnected from the DOM

Note

This situation will probably only occur when writing tests that do not connect elements to the DOM.

LWC components now only fire connectedCallback when they are actually connected to the DOM. If they are disconnected from the DOM (i.e. they are not inside the document), then connectedCallback will not fire.

For instance, if you are testing for an event to be fired from a connectedCallback, this would previously work:

export default class Foo extends LightningElement {
  connectedCallback() {
    this.dispatchEvent(new CustomEvent('foo', { bubbles: true, composed: true }))
  }
}
const element = createElement('x-foo', { is: Foo })
const div = document.createElement('div')
div.addEventListener('foo', () => {
  console.log('received foo event!')
})
div.appendChild(element) // triggers connectedCallback

In the above example, received foo event! would be logged to the console.

After this change, the above no longer triggers the connectedCallback, nor the event.

Instead, you must ensure the element is connected to the DOM:

document.body.appendChild(div)

Once the element is in the DOM tree, connectedCallback will fire as expected.

New error behavior

Note

This situation will probably only occur if you are writing tests to capture errors thrown in connectedCallback/disconnectedCallback. Most component authors should never have to deal with this.

Errors thrown by connectedCallbacks and disconnectedCallback are dispatched globally, rather than locally when elements are added/removed from the DOM.

For example, let's say you have a component that throws an error in connectedCallback:

export default class Foo extends LightningElement {
  connectedCallback() {
      throw new Error('foo')
  }
}

And then you expect to be able to catch the error synchronously on appendChild:

const element = lwc.createElement('x-foo', { is: Foo })
try {
  document.body.appendChild(element)
} catch (error) {
  console.log('caught error', error)
}
  • Old behavior: the error is caught.
  • New behavior: the error is uncaught.

The solution is to use a global error listener:

const listener = (error) => {
  console.log('Caught error', error)
}
try {
  window.addEventListener('error', listener)
  document.body.appendChild(element);
} finally {
  window.removeEventListener('error', listener)
}

If you're using @lwc/jest-*, then you can use the new toThrowInConnectedCallback API which will handle this logic for you.

Note

Events dispatched in connectedCallback and disconnectedCallback are unaffected by this change.

New timing behavior in native shadow DOM

The timing of connectedCallback has changed slightly to align with native browser standards, when using native shadow DOM.

For example, the timing of when connectedCallback fires in child components relative to parent components has changed. Consider this DOM tree:

<x-a>
  #shadow-root
  | <x-a-child>
</x-a>
<x-b>
  #shadow-root
  | <x-b-child>
</x-b>
<x-c>
  #shadow-root
  | <x-c-child>
</x-c>

Old behavior: the order of connectedCallbacks firing is:

  • x-a
  • x-b
  • x-c
  • x-c-child
  • x-b-child
  • x-a-child

New behavior: the order of connectedCallbacks firing is:

  • x-a
  • x-a-child
  • x-b
  • x-b-child
  • x-c
  • x-c-child

Note that, with the new behavior, connectedCallback fires in standard DOM depth-first tree-traversal order. (The old behavior was defined by the synthetic lifecycle polyfill, and did not follow the standard.)

The relative timing of renderedCallback has also changed due the new native connectedCallback timing. For example, consider this DOM tree:

<x-grandparent>
  <x-parent>
    <x-child></x-child>
  </x-parent>
</x-grandparent>

(Note that, in the above example, the <x-parent> and <x-child> are slotted, not part of the shadow roots of their containers.)

Old behavior: the order of lifecycle hooks firing is (example):

Component Lifecycle hook
grandparent connectedCallback
parent connectedCallback
child connectedCallback
child renderedCallback
parent renderedCallback
grandparent renderedCallback

New behavior: the order of lifecycle hooks firing is (example):

Component Lifecycle hook
grandparent connectedCallback
grandparent renderedCallback
parent connectedCallback
parent renderedCallback
child connectedCallback
child renderedCallback

Note that the above only applies to native shadow DOM. In synthetic shadow DOM, the ordering is the same, regardless of native vs synthetic custom element lifecycle (example). The same is true of light DOM (example).

In general, your components should not rely on the particular timing of connectedCallback or renderedCallback across components. In case they do, please update to match the new timing behavior, or make your code agnostic to any particular timing.

Note that the timing of disconnectedCallback has not changed, except as noted elsewhere in these release notes.

Shadow roots may be null after appending to disconnected elements

Consider this case:

<!-- app.html -->
<template>
  <x-child></x-child>
</template>
const elm = createElement("x-app", { is: App })
const div = document.createElement('div')
div.appendChild(elm)

console.log(elm.shadowRoot.querySelector('x-child'))

Old behavior: logs the <x-child> component, because the div.appendChild triggered the rendering of the child element.

New behavior: logs null, because div is disconnected from the DOM, preventing triggering of rendering.

The resolution is to append the element to an element that is actually connected to the DOM. For example, in the above case:

document.body.appendChild(div);

New timing behavior during list rendering

Before LWC v6, certain scenarios with for:each would not result in connectedCallback/disconnectedCallback firing on components inside the iteration, even if the components were actually disconnected and re-connected to the DOM (for example, during list reordering).

In LWC v6, connectedCallback/disconnectedCallback will accurately fire to reflect that the underlying framework has removed and re-inserted elements into the DOM. This may be disruptive for components that do not correctly handle additional connectedCallback/disconnectedCallback invocations, or that have significant performance overhead for these callbacks.

For example, if you have the iteration:

<template for:each={items} for:item="item">
  <x-item key={item}></x-item>
</template>

... and if the items array is originally [1, 2, 3, 4] and later changes to [3, 4, 2, 1], then you would see the following callbacks being invoked after the list order changes:

Old behavior: none of connectedCallback, disconnectedCallback, nor renderedCallback fire after reordering. (Demo)

New behavior: disconnectedCallback fires and then connectedCallback fires for list items 1 and 2 after reordering, because these items have been removed and re-inserted into the end of the list by the LWC framework. (Demo)

This also affects renderedCallback. Previously, you may have relied on renderedCallback for your setup logic and disconnectedCallback for your teardown logic. (For example: setting up event listeners or subscribing to a store.) Previously, this might work for some use cases. However, in LWC v6, your component may fire disconnectedCallback and then connectedCallback if it is inside of a list that is reordered – without firing renderedCallback (if the data bindings inside the component did not change).

For example, consider a list that is reordered from [1, 2] to [2, 1]. In both cases, before reordering, connectedCallback and renderedCallback are fired for items 1 and 2. However, after reordering, the following occurs:

Old behavior: no lifecycle callbacks fire after reordering. (Demo)

New behavior: disconnectedCallback fires and then connectedCallback fires for item 1. Additionally, renderedCallback fires for item 1 but does not fire for its children. (Demo)

A best practice is to do your setup logic in connectedCallback and teardown logic in disconnectedCallback. These two callbacks are perfect mirrors of each other – unlike renderedCallback, which may fire multiple times if the component data changes, and may not fire during a simple list reordering.

Fixed light DOM slot remapping

Note

On the Salesforce Lightning platform, this change only applies to components with an API version of 61 or above.

In previous versions of LWC, light DOM <slot>s do not correctly remap with different names. This bug is fixed in LWC v6, but it may cause a breaking change if you rely on quirks of the previous behavior.

The breaking changes are:

Slot attributes are no longer rendered

In LWC, light DOM slots are virtualized. This means that they do not rely on the native browser <slot> elements or slot attributes at runtime, when the component is actually rendered to the DOM.

Previously, your light DOM elements may have rendered with actual slot attributes. As of LWC v6, these slot attributes will not render. For example, consider this template:

<template>
    <!-- this child is light DOM -->
    <x-child>
        <div slot="foo">hello</div>
    </x-child>
</template>

Previously this would render to the DOM as (demo):

<!-- Previous rendered content -->
<div slot="foo">hello</div>

In LWC v6, this renders as (demo):

<!-- New rendered content -->
<div>hello</div>

If you are relying on the presence of the slot attribute (e.g. in a CSS selector or using this.querySelector()), then your code may break because the slot attribute is no longer present in the rendered DOM.

For light DOM components, you are recommended to avoid CSS selectors or this.querySelector() to match based on the slot attribute. Instead, you can use lwc:ref or choose another CSS selector.

For example, do not rely on CSS selectors like this:

// Don't do this
const foo = this.querySelector('[slot="foo"]');

Instead, use lwc:ref:

<x-child>
    <div slot="foo" lwc:ref="foo">hello</div>
</x-child>
const foo = this.refs.foo;

This may also change your Jest snapshots, which will need to be updated.

Strict light DOM slot forwarding

In previous versions of LWC, light DOM components that "wrap" a shadow DOM component were able to forward to the correct slot in the shadow DOM component without explicitly including a slot attribute.

For example, you may have a light DOM component like this:

<!-- x-slottable-light -->
<template lwc:render-mode=light>
  <x-slottable-shadow>
    <slot name="foo"></slot>
    <slot></slot>
  </x-slottable-shadow>
</template>

In LWC v5, this would forward the foo and default slots from <x-slottable-light> into <x-slottable-shadow>. However, this is actually a mistake, because proper slot forwarding should require a slot attribute on the <slot> element:

 <!-- x-slottable-light -->
 <template lwc:render-mode=light>
   <x-slottable-shadow>
-     <slot name="foo"></slot>
+     <slot name="foo" slot="foo"></slot>
     <slot></slot>
   </x-slottable-shadow>
 </template>

In LWC v6, you must update your code to use the proper slot attribute as shown above, or else the slotted content will not render correctly.

Lifecycle callbacks accurately fire during list rendering

Before LWC v6, certain scenarios in list rendering would not result in connectedCallback/disconnectedCallback firing on components inside the list, even if the components were actually disconnected and re-connected to the DOM (for example, during list reordering).

In LWC v6, connectedCallback/disconnectedCallback will accurately fire to reflect that the underlying framework has removed and re-inserted elements into the DOM. This may be disruptive for components that do not correctly handle additional connectedCallback/disconnectedCallback invocations, or that have significant performance overhead for these callbacks.

For example, if you have the list:

<template for:each={items} for:item="item">
  <x-item uid={item} key={item}></x-item>
</template>

... and if items is originally [1, 2, 3, 4] and later changes to [3, 4, 2, 1]

Demo of LWC v6 behavior compared to LWC v5 behavior.

Standard shadowrootmode attribute in SSR

Note

This change only applies to consumers using SSR (Server Side-Rendering), via the renderComponent() API.

In SSR, the attribute used to mark declarative shadow DOM roots has changed to match the latest browser standards. Instead of shadowroot, the attribute is now shadowrootmode.

This may be a breaking change for you if you have a polyfill that is detecting the shadowroot attribute rather than shadowrootmode. It will also change your Jest snapshots, which will need to be updated.

Old format:

<x-foo>
  <template shadowroot="open">
    <div>Hello world</div>
  </template>
</x-foo>

New format:

 <x-foo>
-  <template shadowroot="open">
+  <template shadowrootmode="open">
     <div>Hello world</div>
   </template>
 </x-foo>

All changes

New Contributors

Full Changelog: v5.3.0...v6.0.0