Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@
"build.core": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --optimizer --insights --qwikrouter --api --platform-binding",
"build.core.dev": "node --require ./scripts/runBefore.ts scripts/index.ts --qwik --optimizer --insights --qwikrouter --platform-binding --dev",
"build.eslint": "node --require ./scripts/runBefore.ts scripts/index.ts --eslint",
"build.full": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding --wasm",
"build.local": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding-wasm-copy",
"build.full": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --optimizer --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding --wasm",
"build.local": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --optimizer --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding-wasm-copy",
"build.only_javascript": "node --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --api",
"build.packages.docs": "pnpm -C ./packages/docs/ run build",
"build.packages.insights": "pnpm -C ./packages/insights/ run build",
Expand Down
36 changes: 11 additions & 25 deletions packages/docs/src/routes/docs/(qwik)/core/each/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import CodeSandbox from '../../../../../components/code-sandbox/index.tsx';

`Each` is a built-in Qwik component for rendering keyed lists.

It is most useful when list items have a stable identity and you want Qwik to preserve and move existing rows instead of re-rendering the whole list when the order changes.
For most list rendering, write normal `items.map()` code and let Qwik optimize compatible keyed loops automatically, either directly to `Each` or through the same keyed runtime internally.

This page documents the manual `Each` API for the cases where you want to use it explicitly. For the default list-rendering guidance, see [Rendering](../rendering/index.mdx).

<CodeSandbox src="/src/routes/demo/component/each/index.tsx" style={{ height: '10em' }}>
```tsx /Each/ /key$/ /item$/
Expand Down Expand Up @@ -76,33 +78,17 @@ export default component$(() => {
});
```

## `Each` vs `items.map()`

If your list items have stable keys, prefer `Each`.

It is the specialized keyed-list primitive in Qwik and is designed to preserve and move existing rows efficiently.

Use `items.map()` for simple list rendering or when you do not have a stable key:

```tsx
<ul>
{todos.value.map((todo) => (
<li key={todo.id}>{todo.label}</li>
))}
</ul>
```
## When to use manual `Each`

Prefer `Each` when:
Most applications should use `items.map()` in component templates.

- items have stable ids
- rows are frequently reordered, inserted, or removed
- you want to preserve existing row DOM and component instances
Manual `Each` is useful when:

Prefer `map()` when:
- you want to make the keyed-list behavior explicit in the source
- you are building a reusable abstraction around `Each`
- you prefer the explicit `items`, `key$`, and `item$` API shape for a specific list

- there is no stable key for the item
- you want ordinary JSX list rendering without `Each`'s keyed-row preservation behavior
- the row output should be recomputed from replaced item objects even when the key stays the same
If you want the usual authoring experience, use `items.map()` and let the optimizer rewrite compatible loops for you.

## How keyed updates work

Expand All @@ -127,5 +113,5 @@ Using unstable keys defeats the main benefit of `Each` and can produce confusing

## Related

- For general list rendering, see [Rendering](../rendering/index.mdx)
- For default list rendering with `items.map()`, automatic optimization, warnings, and opt-out comments, see [Rendering](../rendering/index.mdx)
- For the generated API entry, see [API Reference](/api/qwik/#each)
57 changes: 55 additions & 2 deletions packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ export const Child = component$((props: { name: string }) => {

### Rendering a list of items

Qwik supports rendering lists with either `items.map()` or the [`Each`](../each/index.mdx) component.
Write lists with normal `items.map()` in component templates.

For keyed collections, prefer [`Each`](../each/index.mdx). If you render with `items.map()`, every item in the list must have a unique `key` property on the first child returned by the mapping function. The `key` must be a string or number and must be unique within the list.
If you render with `items.map()`, every item in the list must have a unique `key` property on the first child returned by the mapping function. The `key` must be a string or number and must be unique within the list.

```tsx {6} /data.map/ /key/#a
import { component$ } from '@qwik.dev/core';
Expand All @@ -122,6 +122,59 @@ export const Parent = component$(() => {

<Note>It is not recommended to use the array's index as the key unless you can guarantee that the data for a given key will always be the same. It is always preferred to use some unique identifier from the data as the key.</Note>

## Automatic `.map()` optimization

For compatible render loops, Qwik optimizes keyed `items.map()` calls into the same keyed-list machinery used by [`Each`](../each/index.mdx) automatically during compilation.

The optimization applies when:

- the callback returns a single JSX node
- the first returned node has a `key`
- the key does not use the callback's index parameter
- the key is not derived from a function call

For example, this authoring pattern:

```tsx
{items.value.map((item) => (
<div key={item.id}>{item.label}</div>
))}
```

can be optimized automatically.

For simple capture-free loops, Qwik can lower the render loop directly to `Each`.

When the row render or key expression captures values from the surrounding scope, Qwik can still lower the loop through an internal fallback-assisted path. In that case, Qwik keeps the original `.map()` behavior available and uses keyed `Each` rendering when the captured values are safe for that path.

If a `.map()` looks like a keyed list render but cannot be optimized, Qwik emits a `map-to-each` optimizer warning with the reason. If the conditions are not met, the original `.map()` stays unchanged.

Common reasons for the warning are:

- the returned JSX node is missing a `key`
- the key uses the callback's index parameter
- the key comes from a function call
- the callback does not return a single JSX node

## Disabling the optimization

If you want to opt out for a specific render loop, use:

```tsx
{
/* @qwik-disable-next-line map-to-each */
items.map((item) => <div key={item.id}>{item.label}</div>)
}
```

That disables the optimization and silences `map-to-each` warnings for that call.

## Manual `Each`

Manual [`Each`](../each/index.mdx) is still available as a public API, but it is now the explicit or advanced form rather than the default recommendation.

Use manual `Each` when you want to make the keyed-list behavior explicit in the source or when you are building an abstraction around its `items`, `key$`, and `item$` props.

### Rendering Conditionally

Conditional rendering is done with the Javascipt [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) `?`, the `&&` operator, or just by using `if` statements.
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/src/routes/docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
- [Tasks & Lifecycle](</docs/(qwik)/core/tasks/index.mdx>)
- [Context](</docs/(qwik)/core/context/index.mdx>)
- [Slots](</docs/(qwik)/core/slots/index.mdx>)
- [Each](</docs/(qwik)/core/each/index.mdx>)
- [Rendering](</docs/(qwik)/core/rendering/index.mdx>)
- [Each (Reference)](</docs/(qwik)/core/each/index.mdx>)
- [Styling](</docs/(qwik)/core/styles/index.mdx>)
- [API Reference](/api/qwik/)

Expand Down
7 changes: 6 additions & 1 deletion packages/optimizer/core/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ pub fn transform_code(config: TransformCodeOptions) -> Result<TransformOutput, a
}
program.visit_mut_with(&mut hygiene_with_config(Default::default()));
program.visit_mut_with(&mut fixer(None));
let transform_diagnostics = qt
.as_ref()
.map(|q| q.diagnostics.clone())
.unwrap_or_default();

let mut modules: Vec<TransformModule> = Vec::with_capacity(segments.len() + 10);

Expand Down Expand Up @@ -615,7 +619,8 @@ pub fn transform_code(config: TransformCodeOptions) -> Result<TransformOutput, a
segment: None,
});

let diagnostics = handle_error(&error_buffer, origin, &source_map);
let mut diagnostics = handle_error(&error_buffer, origin, &source_map);
diagnostics.extend(transform_diagnostics);
Ok(TransformOutput {
modules,
diagnostics,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: packages/optimizer/core/src/test.rs
assertion_line: 3978
assertion_line: 3979
expression: output
---
==INPUT==
Expand Down Expand Up @@ -486,4 +486,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma
*/
== DIAGNOSTICS ==

[]
[
{
"category": "warning",
"code": "map-to-each",
"file": "test.tsx",
"message": "This .map() was not optimized to Each because the returned JSX node is missing a key.",
"highlights": [
{
"lo": 1926,
"hi": 2153,
"startLine": 81,
"startCol": 12,
"endLine": 90,
"endCol": 12
}
],
"suggestions": [
"Add a stable key to the returned JSX node."
],
"scope": "optimizer"
}
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: packages/optimizer/core/src/test.rs
assertion_line: 2964
expression: output
---
==INPUT==
Expand Down Expand Up @@ -54,4 +53,25 @@ export const App = /*#__PURE__*/ componentQrl(q_App_component_ckEPmXZlub0);
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;AACA,SAAqB,QAAQ,QAAiB,iBAAiB;;;;kCAIjC;IACd,UAAU;IACzB,MAAM,QAAQ,SAAS,CAAC;IACxB,qBACC,gDACC,WAAC;QAAG,IAAG;OACL,OAAO,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,MAAM,iBACxC,WAAC;YACC;YAAI;YAAI;;AAMd;;AAdA,OAAO,MAAM,oBAAM,0CAchB\"}")
== DIAGNOSTICS ==

[]
[
{
"category": "warning",
"code": "map-to-each",
"file": "test.tsx",
"message": "This .map() was not optimized to Each because the returned JSX node is missing a key.",
"highlights": [
{
"lo": 244,
"hi": 336,
"startLine": 12,
"startCol": 18,
"endLine": 16,
"endCol": 18
}
],
"suggestions": [
"Add a stable key to the returned JSX node."
],
"scope": "optimizer"
}
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: packages/optimizer/core/src/test.rs
assertion_line: 349
expression: output
---
==INPUT==
Expand Down Expand Up @@ -191,4 +190,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma
*/
== DIAGNOSTICS ==

[]
[
{
"category": "warning",
"code": "map-to-each",
"file": "test.tsx",
"message": "This .map() was not optimized to Each because the returned JSX node is missing a key.",
"highlights": [
{
"lo": 403,
"hi": 562,
"startLine": 18,
"startCol": 14,
"endLine": 24,
"endCol": 14
}
],
"suggestions": [
"Add a stable key to the returned JSX node."
],
"scope": "optimizer"
}
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: packages/optimizer/core/src/test.rs
assertion_line: 3326
expression: output
---
==INPUT==
Expand Down Expand Up @@ -4215,4 +4214,45 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind
*/
== DIAGNOSTICS ==

[]
[
{
"category": "warning",
"code": "map-to-each",
"file": "../node_modules/@qwik.dev/router/index.qwik.mjs",
"message": "This .map() was not optimized to Each because the returned JSX node is missing a key.",
"highlights": [
{
"lo": 57097,
"hi": 57156,
"startLine": 1724,
"startCol": 7,
"endLine": 1724,
"endCol": 65
}
],
"suggestions": [
"Add a stable key to the returned JSX node."
],
"scope": "optimizer"
},
{
"category": "warning",
"code": "map-to-each",
"file": "../node_modules/@qwik.dev/router/index.qwik.mjs",
"message": "This .map() was not optimized to Each because the returned JSX node is missing a key.",
"highlights": [
{
"lo": 57164,
"hi": 57224,
"startLine": 1725,
"startCol": 7,
"endLine": 1725,
"endCol": 66
}
],
"suggestions": [
"Add a stable key to the returned JSX node."
],
"scope": "optimizer"
}
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: packages/optimizer/core/src/test.rs
assertion_line: 2367
expression: output
---
==INPUT==
Expand Down Expand Up @@ -120,4 +119,25 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma
*/
== DIAGNOSTICS ==

[]
[
{
"category": "warning",
"code": "map-to-each",
"file": "test.tsx",
"message": "This .map() was not optimized to Each because the returned JSX node is missing a key.",
"highlights": [
{
"lo": 537,
"hi": 583,
"startLine": 20,
"startCol": 10,
"endLine": 22,
"endCol": 10
}
],
"suggestions": [
"Add a stable key to the returned JSX node."
],
"scope": "optimizer"
}
]
Loading