Skip to content
Merged
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 e2e/qwik-e2e/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export {
clientManifest = manifest;
},
},
experimental: ['preventNavigate', 'enableRequestRewrite'],
experimental: ['each', 'preventNavigate', 'enableRequestRewrite'],
}),
],
})
Expand All @@ -227,7 +227,7 @@ export {
plugins: [
...plugins,
optimizer.qwikVite({
experimental: ['preventNavigate', 'enableRequestRewrite'],
experimental: ['each', 'preventNavigate', 'enableRequestRewrite'],
ssr: {
manifestInput: clientManifest,
},
Expand Down
19 changes: 18 additions & 1 deletion packages/docs/src/routes/api/qwik-optimizer/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-vite/src/plugins/bundle-graph.ts",
"mdFile": "qwik-vite.bundlegraphadder.md"
},
{
"name": "each",
"id": "experimentalfeatures-each",
"hierarchy": [
{
"name": "ExperimentalFeatures",
"id": "experimentalfeatures-each"
},
{
"name": "each",
"id": "experimentalfeatures-each"
}
],
"kind": "EnumMember",
"content": "",
"mdFile": "qwik-vite.experimentalfeatures.each.md"
},
{
"name": "enableRequestRewrite",
"id": "experimentalfeatures-enablerequestrewrite",
Expand Down Expand Up @@ -43,7 +60,7 @@
}
],
"kind": "Enum",
"content": "Use `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n<table><thead><tr><th>\n\nMember\n\n\n</th><th>\n\nValue\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nenableRequestRewrite\n\n\n</td><td>\n\n`\"enableRequestRewrite\"`\n\n\n</td><td>\n\nEnable request.rewrite()\n\n\n</td></tr>\n<tr><td>\n\ninsights\n\n\n</td><td>\n\n`\"insights\"`\n\n\n</td><td>\n\nEnable the ability to use the Qwik Insights vite plugin and `<Insights/>` component\n\n\n</td></tr>\n<tr><td>\n\nnoSPA\n\n\n</td><td>\n\n`\"noSPA\"`\n\n\n</td><td>\n\nDisable SPA navigation handler in Qwik Router\n\n\n</td></tr>\n<tr><td>\n\npreventNavigate\n\n\n</td><td>\n\n`\"preventNavigate\"`\n\n\n</td><td>\n\nEnable the usePreventNavigate hook\n\n\n</td></tr>\n<tr><td>\n\nvalibot\n\n\n</td><td>\n\n`\"valibot\"`\n\n\n</td><td>\n\nEnable the Valibot form validation\n\n\n</td></tr>\n<tr><td>\n\nwebWorker\n\n\n</td><td>\n\n`\"webWorker\"`\n\n\n</td><td>\n\nEnable worker$\n\n\n</td></tr>\n</tbody></table>",
"content": "Use `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n<table><thead><tr><th>\n\nMember\n\n\n</th><th>\n\nValue\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\neach\n\n\n</td><td>\n\n`\"each\"`\n\n\n</td><td>\n\nEnable the Each keyed-list primitive\n\n\n</td></tr>\n<tr><td>\n\nenableRequestRewrite\n\n\n</td><td>\n\n`\"enableRequestRewrite\"`\n\n\n</td><td>\n\nEnable request.rewrite()\n\n\n</td></tr>\n<tr><td>\n\ninsights\n\n\n</td><td>\n\n`\"insights\"`\n\n\n</td><td>\n\nEnable the ability to use the Qwik Insights vite plugin and `<Insights/>` component\n\n\n</td></tr>\n<tr><td>\n\nnoSPA\n\n\n</td><td>\n\n`\"noSPA\"`\n\n\n</td><td>\n\nDisable SPA navigation handler in Qwik Router\n\n\n</td></tr>\n<tr><td>\n\npreventNavigate\n\n\n</td><td>\n\n`\"preventNavigate\"`\n\n\n</td><td>\n\nEnable the usePreventNavigate hook\n\n\n</td></tr>\n<tr><td>\n\nvalibot\n\n\n</td><td>\n\n`\"valibot\"`\n\n\n</td><td>\n\nEnable the Valibot form validation\n\n\n</td></tr>\n<tr><td>\n\nwebWorker\n\n\n</td><td>\n\n`\"webWorker\"`\n\n\n</td><td>\n\nEnable worker$\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-vite/src/plugins/plugin.ts",
"mdFile": "qwik-vite.experimentalfeatures.md"
},
Expand Down
15 changes: 15 additions & 0 deletions packages/docs/src/routes/api/qwik-optimizer/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type BundleGraphAdder = (manifest: QwikManifest) => Record<

[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-vite/src/plugins/bundle-graph.ts)

<h2 id="experimentalfeatures-each">each</h2>

<h2 id="experimentalfeatures-enablerequestrewrite">enableRequestRewrite</h2>

<h2 id="experimentalfeatures">ExperimentalFeatures</h2>
Expand Down Expand Up @@ -49,6 +51,19 @@ Description
</th></tr></thead>
<tbody><tr><td>

each

</td><td>

`"each"`

</td><td>

Enable the Each keyed-list primitive

</td></tr>
<tr><td>

enableRequestRewrite

</td><td>
Expand Down
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.
Qwik supports rendering lists with either `items.map()` or the experimental [`Each`](/docs/labs/each/) component.

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.
For keyed collections, prefer [`Each`](/docs/labs/each/) once you have enabled it with `experimental: ['each']`. 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 Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
---
title: Each | Components
title: "\U0001F9EA Each | Experimental"
keywords: 'lists, keyed rendering, control flow, loops'
---

import CodeSandbox from '../../../../../components/code-sandbox/index.tsx';
import CodeSandbox from '../../../../components/code-sandbox/index.tsx';
import { Note } from '~/components/note/note';

# `Each`

**Stage:** `implementation`

<Note>
`Each` is experimental and currently a technical preview. Its API and behavior may still change
as we gather feedback from real-world usage.
</Note>

`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.
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.

To use it, you must add `experimental: ['each']` to your `qwikVite` plugin options:

```ts
// vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@qwik.dev/core/optimizer';

export default defineConfig(() => {
return {
plugins: [
qwikVite({
experimental: ['each'],
}),
],
};
});
```

<CodeSandbox src="/src/routes/demo/component/each/index.tsx" style={{ height: '10em' }}>
```tsx /Each/ /key$/ /item$/
Expand Down Expand Up @@ -54,7 +81,8 @@ export default component$(() => {
- `key$`: returns a stable unique key for each item
- `item$`: returns the JSX for one item

`item$` must return a single JSX node. If you need multiple siblings, wrap them in a container element or a `Fragment`.
`item$` must return a single JSX node. If you need multiple siblings, wrap them in a container
element or a `Fragment`.

```tsx /Fragment/ /Each/
import { Each, Fragment, component$ } from '@qwik.dev/core';
Expand All @@ -80,7 +108,8 @@ export default component$(() => {

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.
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:

Expand All @@ -92,6 +121,10 @@ Use `items.map()` for simple list rendering or when you do not have a stable key
</ul>
```

Qwik may also be able to optimize some keyed `.map()` patterns into `Each` under the hood in the
future when it can prove the transformation is safe. For now, use `Each` explicitly when you want
its keyed-row preservation behavior.

Prefer `Each` when:

- items have stable ids
Expand All @@ -108,13 +141,19 @@ Prefer `map()` when:

`Each` uses the value from `key$` as the identity of the row.

When keys stay the same, Qwik can move and reuse the existing rows instead of recreating them. This is why `Each` is a good fit for drag-and-drop lists, sortable tables, and other UIs where items move around often.
When keys stay the same, Qwik can move and reuse the existing rows instead of recreating them. This
is why `Each` is a good fit for drag-and-drop lists, sortable tables, and other UIs where items
move around often.

There is an important tradeoff: if you replace an item object but keep the same key, `Each` preserves the existing row. That means the row template is not re-run just because a new object with the same key was passed in.
There is an important tradeoff: if you replace an item object but keep the same key, `Each`
preserves the existing row. That means the row template is not re-run just because a new object
with the same key was passed in.

So `Each` is usually the right choice for keyed collections. It has a different update strategy.

If you need the row output to update from replaced item objects, prefer `map()`. If you need row-local updates with `Each`, model that row state with Qwik reactivity instead of relying on replacing the whole item object.
If you need the row output to update from replaced item objects, prefer `map()`. If you need
row-local updates with `Each`, model that row state with Qwik reactivity instead of relying on
replacing the whole item object.

## Keys

Expand All @@ -127,5 +166,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 general list rendering, see [Rendering](/docs/core/rendering/)
- For the generated API entry, see [API Reference](/api/qwik/#each)
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,7 +16,6 @@
- [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>)
- [Styling](</docs/(qwik)/core/styles/index.mdx>)
- [API Reference](/api/qwik/)
Expand Down Expand Up @@ -159,6 +158,7 @@
## Experimental 🧪

- [Overview](/docs/labs/index.mdx)
- [Each](/docs/labs/each/index.mdx)
- [Insights](/docs/labs/insights/index.mdx)
- [Typed Routes](/docs/labs/typed-routes/index.mdx)
- [Devtools](/docs/labs/devtools/index.mdx)
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export default defineConfig(({ mode }) => {
}),
qwikVite({
debug: false,
experimental: ['insights'],
experimental: ['each', 'preventNavigate', 'insights'],
}),
partytownVite({
dest: resolve('dist', '~partytown'),
Expand Down
45 changes: 25 additions & 20 deletions packages/qwik-vite/src/plugins/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const CLIENT_STRIP_CTX_NAME = [
* @public
*/
export enum ExperimentalFeatures {
/** Enable the Each keyed-list primitive */
each = 'each',
/** Enable the usePreventNavigate hook */
preventNavigate = 'preventNavigate',
/** Enable the Valibot form validation */
Expand Down Expand Up @@ -774,6 +776,28 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) {
const dir = parsedPathId.dir;
const base = parsedPathId.base;
const ext = parsedPathId.ext.toLowerCase();

const mode =
opts.target === 'lib'
? 'lib'
: opts.buildMode === 'development'
? devServer?.hot && opts.devTools.hmr
? 'hmr'
: 'dev'
: 'prod';

let didChange = false;
if (mode !== 'lib') {
// this messes a bit with the source map, but it's ok for if statements
code = code.replaceAll(/__EXPERIMENTAL__\.(\w+)/g, (_, feature) => {
didChange = true;
if (opts.experimental?.[feature as ExperimentalFeatures]) {
return 'true';
}
return 'false';
});
}

if (ext in TRANSFORM_EXTS || TRANSFORM_REGEX.test(pathId)) {
/** Strip client|server code from qwik server|client, but not in lib/test */
const strip = opts.target === 'client' || opts.target === 'ssr';
Expand All @@ -782,25 +806,6 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) {
`Transforming ${id} (for: ${isServer ? 'server' : 'client'}${strip ? ', strip' : ''})`
);

const mode =
opts.target === 'lib'
? 'lib'
: opts.buildMode === 'development'
? devServer?.hot && opts.devTools.hmr
? 'hmr'
: 'dev'
: 'prod';

if (mode !== 'lib') {
// this messes a bit with the source map, but it's ok for if statements
code = code.replaceAll(/__EXPERIMENTAL__\.(\w+)/g, (_, feature) => {
if (opts.experimental?.[feature as ExperimentalFeatures]) {
return 'true';
}
return 'false';
});
}

let filePath = base;
if (opts.srcDir) {
filePath = path.relative(opts.srcDir, pathId);
Expand Down Expand Up @@ -890,7 +895,7 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) {

debug(`transform(${count})`, 'Not transforming', id);

return null;
return didChange ? { code } : null;
};

type OutputAnalyzer = {
Expand Down
1 change: 1 addition & 0 deletions packages/qwik-vite/src/qwik.optimizer.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type BundleGraphAdder = (manifest: QwikManifest) => Record<string, {

// @public
export enum ExperimentalFeatures {
each = "each",
enableRequestRewrite = "enableRequestRewrite",
insights = "insights",
noSPA = "noSPA",
Expand Down
7 changes: 6 additions & 1 deletion packages/qwik/src/core/control-flow/each.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,16 @@ export const eachCmpTask = async ({ track }: TaskCtx) => {

/** @internal */
export const eachCmp = (props: EachProps<any>) => {
if (!__EXPERIMENTAL__.each) {
throw new Error(
'Each is experimental and must be enabled with `experimental: ["each"]` in the `qwikVite` plugin.'
);
}
useTaskQrl(/*#__PURE__*/ inlinedQrl(eachCmpTask, '_eaT', [props]));
return SkipRender;
};

/** @public */
/** @public @experimental */
export const Each = /*#__PURE__*/ componentQrl<EachProps<any>>(
/*#__PURE__*/ inlinedQrl(eachCmp, '_eaC')
) as EachComponent;
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default defineConfig({
debug: !true,
srcDir: `./packages/qwik/src`,
devTools: { hmr: false },
experimental: ['each'],
}),
tsconfigPaths({ ignoreConfigErrors: true }),
],
Expand Down
Loading