v6.0.0
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
- Form-Associated Custom Elements (FACE) and Element Internals are now supported.
Summary of breaking change
- Native custom element lifecycle
- Fixed light DOM slot remapping
- Standard
shadowrootmode
attribute in SSR
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
- New error behavior
- New timing behavior in native shadow DOM
- Shadow roots may be null after appending to disconnected elements
- New timing behavior during list rendering
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 connectedCallback
s 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 connectedCallback
s firing is:
x-a
x-b
x-c
x-c-child
x-b-child
x-a-child
New behavior: the order of connectedCallback
s 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 name
s. 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.
- Demo in LWC v5 (working but should not work)
- Demo in LWC v6 (non-working)
- Demo in LWC v6 with corrected code (working)
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
- fix(engine-dom): use native lifecycle callbacks by @nolanlawson in #3904
- fix(engine-server): use standard shadowrootmode attribute by @nolanlawson in #3903
- refactor: fix forwarded light DOM slots mapping by @jmsjtu in #3883
- build(deps): bump follow-redirects from 1.15.3 to 1.15.4 by @dependabot in #3940
- chore: remove event name/type restriction by @ekashida in #3893
- chore: expose jest coverage reports for CI runs @W-14785065 by @wjhsf in #3939
- chore: release v6.0.0 by @jmsjtu in #3946
- chore: release v6.0.0 by @jmsjtu in #3947
New Contributors
Full Changelog: v5.3.0...v6.0.0