diff --git a/e2e/qwik-e2e/dev-server.ts b/e2e/qwik-e2e/dev-server.ts index c50325594a7..a1e952a2c24 100644 --- a/e2e/qwik-e2e/dev-server.ts +++ b/e2e/qwik-e2e/dev-server.ts @@ -211,7 +211,7 @@ export { clientManifest = manifest; }, }, - experimental: ['preventNavigate', 'enableRequestRewrite'], + experimental: ['each', 'preventNavigate', 'enableRequestRewrite'], }), ], }) @@ -227,7 +227,7 @@ export { plugins: [ ...plugins, optimizer.qwikVite({ - experimental: ['preventNavigate', 'enableRequestRewrite'], + experimental: ['each', 'preventNavigate', 'enableRequestRewrite'], ssr: { manifestInput: clientManifest, }, diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index 09c46b80642..01dbc128256 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -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", @@ -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\n\n\n\n\n\n\n
\n\nMember\n\n\n\n\nValue\n\n\n\n\nDescription\n\n\n
\n\nenableRequestRewrite\n\n\n\n\n`\"enableRequestRewrite\"`\n\n\n\n\nEnable request.rewrite()\n\n\n
\n\ninsights\n\n\n\n\n`\"insights\"`\n\n\n\n\nEnable the ability to use the Qwik Insights vite plugin and `` component\n\n\n
\n\nnoSPA\n\n\n\n\n`\"noSPA\"`\n\n\n\n\nDisable SPA navigation handler in Qwik Router\n\n\n
\n\npreventNavigate\n\n\n\n\n`\"preventNavigate\"`\n\n\n\n\nEnable the usePreventNavigate hook\n\n\n
\n\nvalibot\n\n\n\n\n`\"valibot\"`\n\n\n\n\nEnable the Valibot form validation\n\n\n
\n\nwebWorker\n\n\n\n\n`\"webWorker\"`\n\n\n\n\nEnable worker$\n\n\n
", + "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\n\n\n\n\n\n\n\n
\n\nMember\n\n\n\n\nValue\n\n\n\n\nDescription\n\n\n
\n\neach\n\n\n\n\n`\"each\"`\n\n\n\n\nEnable the Each keyed-list primitive\n\n\n
\n\nenableRequestRewrite\n\n\n\n\n`\"enableRequestRewrite\"`\n\n\n\n\nEnable request.rewrite()\n\n\n
\n\ninsights\n\n\n\n\n`\"insights\"`\n\n\n\n\nEnable the ability to use the Qwik Insights vite plugin and `` component\n\n\n
\n\nnoSPA\n\n\n\n\n`\"noSPA\"`\n\n\n\n\nDisable SPA navigation handler in Qwik Router\n\n\n
\n\npreventNavigate\n\n\n\n\n`\"preventNavigate\"`\n\n\n\n\nEnable the usePreventNavigate hook\n\n\n
\n\nvalibot\n\n\n\n\n`\"valibot\"`\n\n\n\n\nEnable the Valibot form validation\n\n\n
\n\nwebWorker\n\n\n\n\n`\"webWorker\"`\n\n\n\n\nEnable worker$\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-vite/src/plugins/plugin.ts", "mdFile": "qwik-vite.experimentalfeatures.md" }, diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.mdx b/packages/docs/src/routes/api/qwik-optimizer/index.mdx index 29e11bfc46a..2e01077b4ad 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.mdx +++ b/packages/docs/src/routes/api/qwik-optimizer/index.mdx @@ -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) +

each

+

enableRequestRewrite

ExperimentalFeatures

@@ -49,6 +51,19 @@ Description +each + + + +`"each"` + + + +Enable the Each keyed-list primitive + + + + enableRequestRewrite diff --git a/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx b/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx index 061f2ac73f9..df632cf002c 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/core/rendering/index.mdx @@ -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'; diff --git a/packages/docs/src/routes/docs/(qwik)/core/each/index.mdx b/packages/docs/src/routes/docs/labs/each/index.mdx similarity index 64% rename from packages/docs/src/routes/docs/(qwik)/core/each/index.mdx rename to packages/docs/src/routes/docs/labs/each/index.mdx index 55289e4e23b..e5d3cd1a558 100644 --- a/packages/docs/src/routes/docs/(qwik)/core/each/index.mdx +++ b/packages/docs/src/routes/docs/labs/each/index.mdx @@ -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` + + + `Each` is experimental and currently a technical preview. Its API and behavior may still change + as we gather feedback from real-world usage. + + `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'], + }), + ], + }; +}); +``` ```tsx /Each/ /key$/ /item$/ @@ -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'; @@ -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: @@ -92,6 +121,10 @@ Use `items.map()` for simple list rendering or when you do not have a stable key ``` +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 @@ -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 @@ -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) diff --git a/packages/docs/src/routes/docs/menu.md b/packages/docs/src/routes/docs/menu.md index 24ab77432b4..c5df915ab6d 100644 --- a/packages/docs/src/routes/docs/menu.md +++ b/packages/docs/src/routes/docs/menu.md @@ -16,7 +16,6 @@ - [Tasks & Lifecycle]() - [Context]() - [Slots]() -- [Each]() - [Rendering]() - [Styling]() - [API Reference](/api/qwik/) @@ -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) diff --git a/packages/docs/vite.config.ts b/packages/docs/vite.config.ts index 88207c97920..206d85d6b61 100644 --- a/packages/docs/vite.config.ts +++ b/packages/docs/vite.config.ts @@ -253,7 +253,7 @@ export default defineConfig(({ mode }) => { }), qwikVite({ debug: false, - experimental: ['insights'], + experimental: ['each', 'preventNavigate', 'insights'], }), partytownVite({ dest: resolve('dist', '~partytown'), diff --git a/packages/qwik-vite/src/plugins/plugin.ts b/packages/qwik-vite/src/plugins/plugin.ts index 612bd4d584e..1f0f6d36083 100644 --- a/packages/qwik-vite/src/plugins/plugin.ts +++ b/packages/qwik-vite/src/plugins/plugin.ts @@ -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 */ @@ -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'; @@ -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); @@ -890,7 +895,7 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) { debug(`transform(${count})`, 'Not transforming', id); - return null; + return didChange ? { code } : null; }; type OutputAnalyzer = { diff --git a/packages/qwik-vite/src/qwik.optimizer.api.md b/packages/qwik-vite/src/qwik.optimizer.api.md index f7dae706875..78f8dc1c872 100644 --- a/packages/qwik-vite/src/qwik.optimizer.api.md +++ b/packages/qwik-vite/src/qwik.optimizer.api.md @@ -19,6 +19,7 @@ export type BundleGraphAdder = (manifest: QwikManifest) => Record { /** @internal */ export const eachCmp = (props: EachProps) => { + 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>( /*#__PURE__*/ inlinedQrl(eachCmp, '_eaC') ) as EachComponent; diff --git a/vitest.config.ts b/vitest.config.ts index 1da2a78c1ba..efcee8c98d2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ debug: !true, srcDir: `./packages/qwik/src`, devTools: { hmr: false }, + experimental: ['each'], }), tsconfigPaths({ ignoreConfigErrors: true }), ],