| title | status | created_at | updated_at | pr |
|---|---|---|---|---|
List rendering reform |
DRAFTED |
2023-11-14 |
2023-11-14 |
This RFC proposes a new template directive, lwc:each, which resolves issues and provides enhancements compared to the existing for:each and iterator:* directives.
<template>
<ul>
<template lwc:each={contacts}
lwc:item="contact"
lwc:key={contact.id}
lwc:index="index"
lwc:first="first"
lwc:last="last"
lwc:even="even"
lwc:odd="odd">
<li>
Name: {contact.name}
Index: {index}
First? {first}
Last? {last}
Even? {even}
Odd? {odd}
</li>
</template>
</ul>
</template>This proposal supersedes the existing for:each and iterator:* directives and resolves several issues with them:
- We have two directives for lists instead of one, with different features available on each. The new directive unifies them.
- The existing directives do not use the
lwc:prefix, unlike every other modern LWC directive. Usinglwc:makes it clear which attributes are LWC-specific, and would allow for improvements in the future such as shorthands (out of scope for this RFC). - The key should be defined on the list root element, not on each element inside the list. The current behavior requires tediously repeating the
keyfor each element inside the list, and also leads to inconsistent behavior for text/comment nodes. - There is no way to easily render based on even vs odd list items.
The design of lwc:each is almost identical to that of for:each. There are only a few modifications.
First, all directives are prefixed with lwc::
for:each→lwc:eachfor:item→lwc:itemkey→lwc:keyfor:index→lwc:index
Second, the lwc:key must be declared on the iterator root, not on each item within the loop:
<template lwc:each={items} lwc:item="item" lwc:key={item.id}>
{item.name}
</template>Third, four new convenience directives are added:
lwc:first- boolean that istrueif the item is first in the listlwc:last- boolean that istrueif the item is last in the listlwc:even- boolean that istrueif the item index is evenlwc:odd- boolean that istrueif the item index is odd
Each directive will be detailed separately.
This functions nearly identically to for:each. (That's kind of the point – it should not be difficult for component authors to migrate.) Like for:each, it is supported in both the <template> format:
<template lwc:each={items} lwc:item="item" lwc:key={item.id}>
{item.name}
</template>…as well as the shorthand, <template>-less format:
<li lwc:each={items} lwc:item="item" lwc:key={item.id}>
{item.name}
</li>Identically to for:each, it supports any Iterable.
Identical to for:item. It is required for lwc:each, and throws a compile-time error if it is not defined:
lwc:each and lwc:item directives should be associated together.
(This is the same error thrown for for:each in the same situation.)
Unlike for:item, lwc:item cannot use the same variable name as lwc:each:
<template lwc:each={foo} lwc:item="foo" lwc:key={foo.id}>
</template>The above will throw a compile-time error, since otherwise the behavior may be unexpected and confusing, especially when considering the lwc:key on the same element. (Which foo does lwc:key={foo.id} refer to above? Rather than guessing, we will throw a compile-time error.)
Identical to key, except that it must be declared on the same element as lwc:each. This avoids the tedious repetition required for key:
<template for:each={items} for:item="item">
<div key={item.id}></div>
<span key={item.id}></span>
<button key={item.id}></button>
</template>With the new directives, this becomes:
<template lwc:each={items} lwc:item="item" lwc:key={item.id}>
<div></div>
<span></span>
<button></button>
</template>Also note that, in the shorthand <template>-less format, lwc:ref is defined not on a <template> but on the same element as the lwc:each:
<li lwc:each={items} lwc:item="item" lwc:key={item.id}>
{item.name}
</li>Similar to key, lwc:key is required on elements with lwc:each, and throws an error if missing:
Missing lwc:key for lwc:each. Iterators must have a unique, computed key value.
Unlike key, a missing lwc:key throws the error regardless of the content inside the iterator. (A missing key will not throw for text nodes, comment nodes, or empty markup – an apparent inconsistency in that directive.)
Similar to key, lwc:key must not reference the variable defined by lwc:index. It also must not reference the variable defined by lwc:first, lwc:last, lwc:even, or lwc:odd. For example, this will throw a compile-time error:
<template lwc:each={items} lwc:item="item" lwc:index="idx" lwc:key={idx}>
</template>In short, lwc:key must reference the lwc:item variable. Anything else throws a compile-time error.
Identical to for:index. Returns a number for the current list index.
As with for:index, lwc:index cannot use the same string as the lwc:item on the same element:
<template lwc:each={items} lwc:key={same.id} lwc:item="same" lwc:index="same">
</template>The above will throw a compile-time error.
(Note that for:index only incidentally throws an error for the same case, because the key ends up referencing the for:index variable. For lwc:index, the compile-time error should be explicit.)
These new directives take strings as their values. When referenced, they resolve to a boolean representing whether the list item is first, last, even, or odd. (This is heavily inspired by Angular.)
The addition of lwc:first and lwc:last serve to unify the two existing directives – previously, iterator:* was the only way to get "first" or "last" behavior. The addition of lwc:even and lwc:odd are merely for convenience (especially in the absence of complex template expressions).
All new directives that take a string as their value (lwc:item, lwc:index, lwc:first, lwc:last, lwc:even, and lwc:odd) must have unique values when used on the same element.
For example, the following will throw a compile-time error, because lwc:item and lwc:index have the same values:
<template lwc:each={items} lwc:key={same.id} lwc:item="same" lwc:index="same">
</template>Note that this restriction does not apply to loops within loops. Just like for:each, variables in inner loops simply shadow the same variable in outer loops:
<template lwc:each={outer} lwc:key={item} lwc:item="item" lwc:index="index">
<!-- These will be the outer loop variables -->
Outer item: {item}
Outer index: {index}
<template lwc:each={inner} lwc:key={item} lwc:item="item" lwc:index="index">
<!-- These will be the inner loop variables -->
Inner item: {item}
Inner index: {index}
</template>
</template>Note that the two loops have the same variable names for lwc:item and lwc:index, and this does not cause a compiler error.
Also note that the value of the lwc:key in each case refers to the item on the same <template> element.
Empty values (e.g. lwc:index="") will also throw a compile-time error. For example:
<template lwc:each={items} lwc:key={item.id} lwc:item="item" lwc:index="">
</template>Missing values (e.g. lwc:index) will also throw at compile-time:
<template lwc:each={items} lwc:key={item.id} lwc:item="item" lwc:index>
</template>The directives may appear in any order on the element.
Any other constraints that apply to for:each must also apply to lwc:each. For example, lwc:ref is (currently) unsupported inside of for:each and iterator:* – the same applies to lwc:each. For another example, <slot>s cannot have any of the new directives, and cannot be repeated inside the new iterator.
This new directive may cause confusion, because we are introducing a third directive to try to unify the previous two (see XKCD 927).
The implementation also becomes more complex, as we will need to support all three directives for the foreseeable future. Perhaps they can share logic under the hood, but we will still need to test all three, at the very least.
We will also need to make an effort to advocate for adoption of the new directive, which may involve updating existing documentation and writing codemods or IDE tooling.
One reasonable alternative would be to enhance the existing for:each rather than creating a new directive. However, this has several drawbacks:
- There could be conflicts between the legacy
keyon within-iterator elements versus thelwc:keyon the iterator element. In cases of nested iterators, it could be very tricky to determine which keys should apply to which iterators. - It does not move us closer to a world where all LWC directives use the
lwc:prefix. Directives likefor:each,for:item,for:index, andkeywould continue to be the way to write iterators, making it difficult to support a directive shorthand in the future.
Similarly, enhancing iterator:* is another approach. However, this directive is seldom-used, and is rigid in how it allows for referencing the value, index, and first and last items in a list (the property names must always be value, index, first, and last).
The key is a performance optimization, and not all developers need it. Some developers even use a getter with Math.random() to provide a random key, to work around the current LWC requirement that lists must have a key.
It is entirely possible to make lwc:key optional. However, this comes with a few downsides:
- Developers may be encouraged to use a less-performant rendering pattern, since it takes less effort to omit the key.
- It deviates from the existing behavior of
for:each, and is one more thing to teach. - Developers may now assume that keys are actually a bad idea, since the "improved"
lwc:eachmakes them optional.
So, for this proposal, keys are still required. However, it is definitely something that can be revisited in the future.
Similar to lwc:if/lwc:elseif/lwc:else, the messaging will be that lwc:each is the "new" way to use lists in LWC templates, and component authors should migrate from for:each and iterator:*.
Unlike lwc:if and friends, there are no feature gaps between lwc:each and the existing for:each and iterator:* directives. (lwc:if is missing an equivalent to if:false, at least in the absence of complex expressions.) Therefore, it should be relatively straightforward to migrate existing components – it could even be done with lwc-codemod.
In the future, we could also use component-level API versioning to disallow the legacy for:each and iterator:* directives.
The new lwc:each directive is essentially a superset of for:each, so it can leverage most of the teaching material for that directive.
In addition, we can downplay or remove documentaiton for for:each and iterator:*, simplifying the LWC education narrative.
None at this time.