From 83b87ca25d25f716b1f11b4d48e678213ea11c8c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 00:59:35 +0000 Subject: [PATCH 1/3] reference: regenerate against solid next eb4e0192; dispositions for rc.14-rc.15 exports New pages: markRaw, onHydrationEnd, dispatchAsInteraction. dynamicComponent, lazyModule and reportRequestFailure join the dynamic, lazy and configureServerErrors pages; ssrStyleProperties is compiler ABI and hidden. The NoArray brand on the seedless derived-store form is dropped from rendered signatures. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y4HXPRqXPnqriNxDyin6eK --- scripts/extract-solid-ref.mjs | 22 ++++ .../(1)reactivity/create-effect.mdx | 2 +- .../(1)reactivity/create-signal.mdx | 18 +-- .../(2)stores/create-optimistic-store.mdx | 12 +- .../(2)stores/create-projection.mdx | 14 ++- .../(1)solid-js/(2)stores/create-store.mdx | 12 +- .../(3)lifecycle-actions/action.mdx | 11 +- .../(3)lifecycle-actions/affects.mdx | 10 +- .../(3)lifecycle-actions/refresh.mdx | 11 +- .../(3)lifecycle-actions/until.mdx | 19 +--- .../(4)components-context/lazy.mdx | 68 ++++++++++-- .../create-render-effect.mdx | 2 +- .../(3)store-advanced/is-wrappable.mdx | 2 +- .../(3)store-advanced/mark-raw.mdx | 44 ++++++++ .../(5)manual-hydration/on-hydration-end.mdx | 78 +++++++++++++ .../(6)advanced/(6)interop-async/flatten.mdx | 28 +---- .../(7)diagnostics-dev-hooks/attribution.mdx | 48 ++++++-- .../(7)diagnostics-dev-hooks/dev.mdx | 3 + .../dispatch-as-interaction.mdx | 68 ++++++++++++ .../(7)diagnostics-dev-hooks/observe.mdx | 9 +- .../(1)solid-js/(7)types/store-types.mdx | 2 +- .../(2)solid-web/(3)components/dynamic.mdx | 105 ++++++++++++++++-- .../(5)server-functions/configure-client.mdx | 21 +++- .../configure-server-errors.mdx | 42 +++++-- .../(6)request-response/respond.mdx | 16 ++- .../enable-performance-tracks.mdx | 14 ++- 26 files changed, 547 insertions(+), 134 deletions(-) create mode 100644 src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/mark-raw.mdx create mode 100644 src/routes/reference/(1)solid-js/(6)advanced/(5)manual-hydration/on-hydration-end.mdx create mode 100644 src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dispatch-as-interaction.mdx diff --git a/scripts/extract-solid-ref.mjs b/scripts/extract-solid-ref.mjs index e11ee1faf..cfa223888 100644 --- a/scripts/extract-solid-ref.mjs +++ b/scripts/extract-solid-ref.mjs @@ -133,7 +133,9 @@ const CANONICAL_ROUTES = { "Components & Context", ], dynamic: ["web-components/dynamic.mdx", "Components"], + dynamicComponent: ["web-components/dynamic.mdx", "Components"], lazy: ["components-context/lazy.mdx", "Components & Context"], + lazyModule: ["components-context/lazy.mdx", "Components & Context"], useContext: ["components-context/use-context.mdx", "Components & Context"], Errored: ["components-jsx/errored.mdx", "Components (JSX)"], @@ -198,6 +200,10 @@ const CANONICAL_ROUTES = { "request-response/configure-server-errors.mdx", "Request & response", ], + reportRequestFailure: [ + "request-response/configure-server-errors.mdx", + "Request & response", + ], redirect: ["request-response/redirect.mdx", "Request & response"], reload: ["request-response/reload.mdx", "Request & response"], respond: ["request-response/respond.mdx", "Request & response"], @@ -261,6 +267,10 @@ const ADVANCED_ROUTES = { "advanced/store-advanced/snapshot.mdx", "Advanced / Store Advanced", ], + markRaw: [ + "advanced/store-advanced/mark-raw.mdx", + "Advanced / Store Advanced", + ], storePath: [ "advanced/store-advanced/store-path.mdx", "Advanced / Store Advanced", @@ -291,6 +301,10 @@ const ADVANCED_ROUTES = { "advanced/manual-hydration/no-hydration.mdx", "Advanced / Manual Hydration", ], + onHydrationEnd: [ + "advanced/manual-hydration/on-hydration-end.mdx", + "Advanced / Manual Hydration", + ], enableExternalSource: [ "advanced/interop-async/enable-external-source.mdx", @@ -315,6 +329,10 @@ const ADVANCED_ROUTES = { "advanced/diagnostics-dev-hooks/configure-client-errors.mdx", "Advanced / Diagnostics & Dev Hooks", ], + dispatchAsInteraction: [ + "advanced/diagnostics-dev-hooks/dispatch-as-interaction.mdx", + "Advanced / Diagnostics & Dev Hooks", + ], attribution: [ "advanced/diagnostics-dev-hooks/attribution.mdx", "Advanced / Diagnostics & Dev Hooks", @@ -667,6 +685,7 @@ const HIDDEN_EXPORTS = new Set([ "ssrHydrationKey", "ssrRunInScope", "ssrStyle", + "ssrStyleProperties", "template", // Compiler and DOM-runtime ABI. These exports support generated code and // renderer integrations rather than application code. @@ -2604,6 +2623,9 @@ const TYPE_TEXT_REWRITES = [ [/\bHydrationMemoOptions]+)>/g, "$1"], + // The seedless derived form rejects array roots with an `& NoArray` + // brand on its function; the prose says so, the signature need not. + [/\s*&\s*NoArray<[^<>]+>/g, ""], [/\bRefreshable<((?:[^<>]|<[^<>]*>)+)>/g, "$1"], // The core `Element` type is imported as `SolidElement` inside the // package; app code knows it as `JSX.Element`. diff --git a/src/routes/reference/(1)solid-js/(1)reactivity/create-effect.mdx b/src/routes/reference/(1)solid-js/(1)reactivity/create-effect.mdx index 67959a93a..4e64c3f18 100644 --- a/src/routes/reference/(1)solid-js/(1)reactivity/create-effect.mdx +++ b/src/routes/reference/(1)solid-js/(1)reactivity/create-effect.mdx @@ -49,7 +49,7 @@ import { createEffect } from "solid-js"; ## Type signature ```ts -function createEffect( +function createEffect( compute: ComputeFunction, T>, effectFn: EffectFunction, T> | EffectBundle, T>, options?: EffectOptions diff --git a/src/routes/reference/(1)solid-js/(1)reactivity/create-signal.mdx b/src/routes/reference/(1)solid-js/(1)reactivity/create-signal.mdx index a9aa64bfb..cf8379355 100644 --- a/src/routes/reference/(1)solid-js/(1)reactivity/create-signal.mdx +++ b/src/routes/reference/(1)solid-js/(1)reactivity/create-signal.mdx @@ -38,10 +38,11 @@ setCount(1); // explicit value setCount((c) => c + 1); // updater // Writable memo: derives from `fn()` and can be written like a signal. -// Within the frame the write wins over a same-tick recompute; the next -// change to `userId` re-derives (the compute receives the written value -// as `prev`). While a transaction holds a re-derived value, a write from -// outside it does not replace that derivation — it becomes its `prev`. +// A write lands at once and never re-runs `fn` on its own; when `userId` +// changes, in the same update or later, `fn` re-runs and receives the +// written value as `prev`. While a transaction holds a re-derived value, a +// write from outside it does not replace that derivation — it becomes its +// `prev`. const [user, setUser] = createSignal(() => fetchUser(userId())); setUser({ ...user(), name: "Alice" }); ``` @@ -116,10 +117,11 @@ setCount((c) => c + 1); // updater // Writable memo: derives from `fn()` and can be written like a signal. const [user, setUser] = createSignal(() => fetchUser(userId())); -// Within the frame the write wins over a same-tick recompute; the next -// change to `userId` re-derives (the compute receives the written value -// as `prev`). While a transaction holds a re-derived value, a write from -// outside it does not replace that derivation — it becomes its `prev`. +// A write lands at once and never re-runs `fn` on its own; when `userId` +// changes, in the same update or later, `fn` re-runs and receives the +// written value as `prev`. While a transaction holds a re-derived value, a +// write from outside it does not replace that derivation — it becomes its +// `prev`. setUser({ ...user(), name: "Alice" }); ``` diff --git a/src/routes/reference/(1)solid-js/(2)stores/create-optimistic-store.mdx b/src/routes/reference/(1)solid-js/(2)stores/create-optimistic-store.mdx index f667a975d..d3b404613 100644 --- a/src/routes/reference/(1)solid-js/(2)stores/create-optimistic-store.mdx +++ b/src/routes/reference/(1)solid-js/(2)stores/create-optimistic-store.mdx @@ -49,9 +49,14 @@ function createOptimisticStore( initialValue: T | Store, options?: StoreOptions ): [get: Store, set: StoreSetter]; +function createOptimisticStore( + fn: () => T | Promise | AsyncIterable, + seed?: null, + options?: Omit +): [get: Store, set: StoreSetter]; function createOptimisticStore( fn: (draft: T) => void | T | Promise | AsyncIterable, - seed: Partial | Store, + seed: T | Store, options?: ProjectionOptions ): [get: Store, set: StoreSetter]; ``` @@ -73,13 +78,14 @@ Plain object or array to wrap. ### `fn` -- **Type:** `(draft: T) => void | T | Promise | AsyncIterable` +- **Type:** `(() => T | Promise | AsyncIterable) | (draft: T) => void | T | Promise | AsyncIterable` Derive function that produces the authoritative contents. Optimistic writes overlay its result and revert when the action settles. ### `seed` -- **Type:** `Partial | Store` +- **Type:** `null | T | Store` +- Optional Initial contents of the derived form. diff --git a/src/routes/reference/(1)solid-js/(2)stores/create-projection.mdx b/src/routes/reference/(1)solid-js/(2)stores/create-projection.mdx index eec811a92..0ac36da85 100644 --- a/src/routes/reference/(1)solid-js/(2)stores/create-projection.mdx +++ b/src/routes/reference/(1)solid-js/(2)stores/create-projection.mdx @@ -40,9 +40,14 @@ import { createProjection } from "solid-js"; ## Type signature ```ts +function createProjection( + fn: () => T | Promise | AsyncIterable, + seed?: null, + options?: Omit +): Store; function createProjection( fn: (draft: T) => void | T | Promise | AsyncIterable, - seed: Partial | Store, + seed: T | Store, options?: ProjectionOptions ): Store; ``` @@ -51,19 +56,20 @@ function createProjection( ### `fn` -- **Type:** `(draft: T) => void | T | Promise | AsyncIterable` +- **Type:** `(() => T | Promise | AsyncIterable) | (draft: T) => void | T | Promise | AsyncIterable` Derive function. Receives a draft and either mutates it or returns a new value; may be sync, async, or an `AsyncIterable`. The result is reconciled by `options.key`. ### `seed` -- **Type:** `Partial | Store` +- **Type:** `null | T | Store` +- Optional Initial contents and the draft the first derive receives. With an async `fn`, readers see the seed before the first result only when `seedLoadingValue` is set; otherwise they suspend. ### `options` -- **Type:** `ProjectionOptions` +- **Type:** `Omit | ProjectionOptions` - Optional `key` (default `"id"`), `seedLoadingValue`, `name`, `shallow`, `deferStream`, and `ssrSource`. diff --git a/src/routes/reference/(1)solid-js/(2)stores/create-store.mdx b/src/routes/reference/(1)solid-js/(2)stores/create-store.mdx index ee7b0d14f..f7613a1b9 100644 --- a/src/routes/reference/(1)solid-js/(2)stores/create-store.mdx +++ b/src/routes/reference/(1)solid-js/(2)stores/create-store.mdx @@ -59,9 +59,14 @@ function createStore( initialValue: T | Store, options?: StoreOptions ): [get: Store, set: StoreSetter]; +function createStore( + fn: () => T | Promise | AsyncIterable, + seed?: null, + options?: Omit +): [get: Store, set: StoreSetter]; function createStore( fn: (draft: T) => void | T | Promise | AsyncIterable, - seed: Partial | Store, + seed: T | Store, options?: ProjectionOptions ): [get: Store, set: StoreSetter]; ``` @@ -83,13 +88,14 @@ Plain object or array to wrap, or an existing store to share. ### `fn` -- **Type:** `(draft: T) => void | T | Promise | AsyncIterable` +- **Type:** `(() => T | Promise | AsyncIterable) | (draft: T) => void | T | Promise | AsyncIterable` Derive function for the projection form. Receives a draft and either mutates it or returns a new value; may be sync, async, or an `AsyncIterable`. ### `seed` -- **Type:** `Partial | Store` +- **Type:** `null | T | Store` +- Optional Initial contents of the projection store and the draft the first derive receives. With an async `fn`, readers see the seed before the first result only when `seedLoadingValue` is set; otherwise they suspend. diff --git a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/action.mdx b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/action.mdx index 620255497..37914d308 100644 --- a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/action.mdx +++ b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/action.mdx @@ -35,9 +35,9 @@ the same transactional semantics — with form binding, serialization, and submission tracking layered on top. The shared name is deliberate. Wraps a generator function so each invocation runs as a single transaction -that batches every signal/store write between yields. The -surrounding UI sees one atomic update per yielded step; nothing is committed -until the action either completes or the next `yield` resolves. +that holds every signal/store write between yields. The surrounding UI sees +one atomic update when the action completes; nothing is committed until +every action in the transaction has returned. `yield` is the transaction-safe suspension point: the action waits for a yielded promise and re-enters the transaction before running the code after @@ -47,8 +47,7 @@ the next `yield` runs OUTSIDE the transaction: writes to fresh signals commit immediately, and anything that creates a reader there — `until()`, `latest()`, a memo or effect, a mount — is created mainline, where a read of this action's held state makes it born held (A29): staged with the -transaction and replayed at its commit. For `until()` that commit is the -settle its own promise holds open (#3482). `await` is still the ergonomic +transaction and replayed at its commit. `await` is still the ergonomic choice for typed results; just put a bare `yield` before any write or reader creation that follows it — including the expression of the next `yield`, which is evaluated before the step re-enters: @@ -61,7 +60,7 @@ yield until(() => todos.some(t => t.id === saved.id)); ``` (For the same reason, don't call `flush()` inside an action body — it -drains the transaction mid-step.) +cannot reveal the held writes and is refused.) Each call returns a `Promise` that resolves with the generator's return value, or rejects if it throws. Pair with `createOptimistic` / diff --git a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/affects.mdx b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/affects.mdx index 3db37a7d6..35033398b 100644 --- a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/affects.mdx +++ b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/affects.mdx @@ -50,16 +50,8 @@ When `target` is a store, the property to mark instead of the whole store. ## Examples ```ts -const send = action(function* (text: string) { - setState((s) => { - s.messages.push({ text, status: "sending" }); - }); - affects(state.messages.at(-1)!, "status"); // this slot pends until settle - yield api.send(text); -}); - const reload = action(function* () { - affects(thing); // the whole store pends… + affects(thing); // `thing` and its derivations pend… refresh(thing); // …over this otherwise-quiet re-ask yield api.done(); }); diff --git a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/refresh.mdx b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/refresh.mdx index c50a26bb6..48581fa26 100644 --- a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/refresh.mdx +++ b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/refresh.mdx @@ -33,19 +33,10 @@ read: - Accessor targets resolve with the settled value; store targets resolve with the store node passed (reads through it are fresh after the await). -- A failed re-ask rejects with the error (inside an action's generator, - `yield refresh(x)` throws back at the yield point and the action reverts - like any other failure). +- A failed re-ask rejects with the error. - Semantics are quiescence, not flight identity: if another refresh (or any invalidation) supersedes this one mid-flight, the promise waits for — and delivers — whatever finally lands. -- Inside an action, truth landing into the held transaction is staged; - the promise still settles then (matching `until()`) - and delivers the staged value — the caller's own optimistic override is - never the delivered value. -- The re-ask itself stays verdict-quiet exactly as before: `isPending` - does not flip for a bare refresh (pair with `affects()` for a visible - pending window). ## Import diff --git a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/until.mdx b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/until.mdx index 7856dde71..ac268c3c7 100644 --- a/src/routes/reference/(1)solid-js/(3)lifecycle-actions/until.mdx +++ b/src/routes/reference/(1)solid-js/(3)lifecycle-actions/until.mdx @@ -38,7 +38,7 @@ function until(fn: () => T, options?: UntilOptions): Promise>; - **Type:** `() => T` -A reactive predicate over authoritative state +A reactive predicate ### `options` @@ -54,18 +54,11 @@ A promise that resolves with the first truthy result of `fn`, narrowed by `Truth ## Examples ```ts -const send = action(async function* (text: string) { - const clientId = crypto.randomUUID(); - setMessages((m) => { - m.push({ clientId, text, pending: true }); - }); // optimistic - await socket.send({ clientId, text }); // fire-and-forget transport - yield; // re-enter the transaction after the await - // Hold until the live source echoes the write (authoritative view — - // the optimistic row above cannot satisfy this): - yield until(() => messages.some((m) => m.clientId === clientId), { - timeout: 10_000, - }); +const clientId = crypto.randomUUID(); +await socket.send({ clientId, text }); // fire-and-forget transport +// Wait until the live source echoes the write: +await until(() => messages.some((m) => m.clientId === clientId), { + timeout: 10_000, }); ``` diff --git a/src/routes/reference/(1)solid-js/(4)components-context/lazy.mdx b/src/routes/reference/(1)solid-js/(4)components-context/lazy.mdx index f2f486fc1..276673a92 100644 --- a/src/routes/reference/(1)solid-js/(4)components-context/lazy.mdx +++ b/src/routes/reference/(1)solid-js/(4)components-context/lazy.mdx @@ -1,9 +1,10 @@ --- -title: "lazy" +title: "lazy / lazyModule" category: "Components & Context" use_cases: "components & context api, lazy usage" tags: - "lazy" + - "module" - "components" - "context" - "reference" @@ -26,10 +27,12 @@ import early (e.g. on hover). ## Import ```ts -import { lazy } from "solid-js"; +import { lazy, lazyModule } from "solid-js"; ``` -## Type signature +## `lazy` + +### Type signature ```ts function lazy, K extends keyof M & string>( @@ -44,33 +47,33 @@ function lazy>( ): T & { preload: () => Promise<{ default: T }>; moduleUrl?: string }; ``` -## Parameters +### Parameters -### `fn` +#### `fn` - **Type:** `() => Promise | () => Promise<{ default: T }>` Dynamic import that resolves the component's module. -### `options` +#### `options` - **Type:** `{ export: K } | { export?: string }` - Optional `{ export }` names which export of the module is the component. Defaults to `default`. -### `moduleUrl` +#### `moduleUrl` - **Type:** `string` - Optional Module specifier injected by the bundler integration. Exposed as the component's `moduleUrl` property and used in hydration error messages. -## Return value +### Return value A component with the same props as the imported one, plus a `preload()` method that starts the import early. -## Examples +### Examples ```tsx const Profile = lazy(() => import("./Profile")); @@ -88,11 +91,56 @@ function App() { ; ``` -## Caveats +### Caveats - The import runs on first render, not at definition. Wrap the component in a `Loading` boundary or the nearest boundary shows its fallback. - By default the component must be the module's default export. Use `{ export: "Name" }` for a named export; a runtime wrapper that selects one breaks hydration. +## `lazyModule` + +A cached dynamic import the server can preload before hydration, for a +module that is not itself a component. `lazy()` is the component-shaped +sibling and is unchanged. + +Call the result (or `.preload()`) to start the import. Both share one +promise. `.peek()` reads it synchronously: the settled module, or +`undefined` when it has not landed. During hydration `.peek()` reads +`_$HY.modules[moduleUrl]` and returns `undefined` on a miss — it does not +throw. The bundler pass appends `moduleUrl` for a literal `import()`. + +### Type signature + +```ts +function lazyModule>( + fn: () => Promise, + moduleUrl?: string +): (() => Promise) & { + preload: () => Promise; + peek: () => T | undefined; + moduleUrl?: string; +}; +``` + +### Parameters + +#### `fn` + +- **Type:** `() => Promise` + +#### `moduleUrl` + +- **Type:** `string` +- Optional + +### Examples + +```ts +const admin = lazyModule(() => import("./admin/routes")); +admin(); // Promise, cached +admin.preload(); // same promise +admin.peek(); // Module | undefined +``` + ## Learn more - [Loading boundaries](/concepts/boundaries#loading-boundaries) diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(2)specialized-reactivity/create-render-effect.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(2)specialized-reactivity/create-render-effect.mdx index 58828e4ff..fffba7e18 100644 --- a/src/routes/reference/(1)solid-js/(6)advanced/(2)specialized-reactivity/create-render-effect.mdx +++ b/src/routes/reference/(1)solid-js/(6)advanced/(2)specialized-reactivity/create-render-effect.mdx @@ -52,7 +52,7 @@ import { createRenderEffect } from "solid-js"; ## Type signature ```ts -function createRenderEffect( +function createRenderEffect( compute: ComputeFunction, T>, effectFn: EffectFunction, T>, options?: EffectOptions diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/is-wrappable.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/is-wrappable.mdx index 4f12b3acf..cb5bf9c40 100644 --- a/src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/is-wrappable.mdx +++ b/src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/is-wrappable.mdx @@ -14,7 +14,7 @@ version: "2.0" description: "Returns whether a value would be wrapped in a store proxy." source_repo: "solidjs/solid" source_ref: "next" -source_path: "packages/signals/src/store/store.ts" +source_path: "packages/signals/src/store/types.ts" --- {/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/mark-raw.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/mark-raw.mdx new file mode 100644 index 000000000..68223c12d --- /dev/null +++ b/src/routes/reference/(1)solid-js/(6)advanced/(3)store-advanced/mark-raw.mdx @@ -0,0 +1,44 @@ +--- +title: "markRaw" +category: "Advanced / Store Advanced" +use_cases: "advanced store advanced api, markraw usage" +tags: + - "mark" + - "raw" + - "advanced" + - "store" + - "reference" + - "api" + - "v2" +version: "2.0" +description: "Mark a value so no store ever wraps it (sticky; it reads as a leaf)." +source_repo: "solidjs/solid" +source_ref: "next" +source_path: "packages/signals/src/store/types.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Mark a value so no store ever wraps it (sticky; it reads as a leaf). + +## Import + +```ts +import { markRaw } from "@solidjs/signals"; +``` + +## Type signature + +```ts +function markRaw(value: T): T; +``` + +## Parameters + +### `value` + +- **Type:** `T` + +## Learn more + +- [Stores](/concepts/stores) diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(5)manual-hydration/on-hydration-end.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(5)manual-hydration/on-hydration-end.mdx new file mode 100644 index 000000000..4e7760ec9 --- /dev/null +++ b/src/routes/reference/(1)solid-js/(6)advanced/(5)manual-hydration/on-hydration-end.mdx @@ -0,0 +1,78 @@ +--- +title: "onHydrationEnd" +category: "Advanced / Manual Hydration" +use_cases: "advanced manual hydration api, onhydrationend usage" +tags: + - "on" + - "hydration" + - "end" + - "advanced" + - "manual" + - "reference" + - "api" + - "v2" +version: "2.0" +description: "Runs `callback` once, when the hydration phase ends: the root pass of `hydrate()` is over and every streamed `` boundary (and every hold an integration registers, such as server-component content waiting for its data) has resumed or been cancelled." +source_repo: "solidjs/solid" +source_ref: "next" +source_path: "packages/solid/src/client/hydration.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Runs `callback` once, when the hydration phase ends: the root pass of +`hydrate()` is over and every streamed `` boundary (and every +hold an integration registers, such as server-component content waiting +for its data) has resumed or been cancelled. When no hydration is in +progress — before `hydrate()` starts, after the phase has ended, in a +client-only render, on the server — it runs in the next microtask. + +Intended for test tooling and markers, such as an attribute an end-to-end +test waits for. Do not branch what the app renders on it: read +`isHydrating()` where a value must match the server, and switch to the +client-only value from `onSettled`. + +- **Register it after calling `hydrate()`.** Registered before hydration + starts, the callback runs in the next microtask and does not wait for + hydration work started afterward. The same holds when `hydrate()` parks + because the entry ran while the parser was still inside the server + shell (an `async` entry script): hydration starts at the shell-end + marker, after `hydrate()` has returned. +- **It does not say hydration succeeded.** When a document root's module + preload fails, hydration is abandoned with the server markup left in + place and not interactive, and the callback still runs. A non-document + root whose preload fails runs it before falling back to a client render. +- **It is not page-wide completion with several hydration roots.** A root + whose render waits on its module preload is not counted, so another + root finishing can end the phase before that root has claimed anything. + +## Import + +```ts +import { onHydrationEnd } from "solid-js"; +``` + +## Type signature + +```ts +function onHydrationEnd(callback: () => void): void; +``` + +## Parameters + +### `callback` + +- **Type:** `() => void` + +## Examples + +```ts +hydrate(() => , document); +onHydrationEnd(() => { + document.documentElement.dataset.hydrated = ""; +}); +``` + +## Learn more + +- [Controlling hydration](/concepts/rendering-and-ssr#controlling-hydration) diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(6)interop-async/flatten.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(6)interop-async/flatten.mdx index ac6b1716b..eb2705c2c 100644 --- a/src/routes/reference/(1)solid-js/(6)advanced/(6)interop-async/flatten.mdx +++ b/src/routes/reference/(1)solid-js/(6)advanced/(6)interop-async/flatten.mdx @@ -11,21 +11,16 @@ tags: - "api" - "v2" version: "2.0" -description: 'Resolves a children value to its renderable form: unwraps zero-arg functions (accessors), recursively flattens arrays, and optionally skips non-rendering values (`null`, `undefined`, `true`, `false`, `""`).' +description: "Resolves nested accessors and arrays in a children tree to their rendered values. See the `children()` helper in solid-js for the memoized form." source_repo: "solidjs/solid" source_ref: "next" -source_path: "packages/signals/src/boundaries.ts" +source_path: "packages/signals/src/flatten.ts" --- {/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} -Resolves a children value to its renderable form: unwraps zero-arg functions -(accessors), recursively flattens arrays, and optionally skips -non-rendering values (`null`, `undefined`, `true`, `false`, `""`). - -Used internally by flow components and by the renderer to walk a children -tree. App code rarely needs this directly — see `children()` in `solid-js` -for the user-facing helper that memoizes the result. +Resolves nested accessors and arrays in a children tree to their rendered +values. See the `children()` helper in solid-js for the memoized form. ## Import @@ -48,26 +43,11 @@ function flatten( - **Type:** `any` -Value or array of values to flatten - ### `options` - **Type:** `{ skipNonRendered?: boolean; doNotUnwrap?: boolean }` - Optional -- `skipNonRendered` — drop values that won't render - - `doNotUnwrap` — leave function children as-is (caller will resolve) - -## Examples - -```ts -// Custom renderer walking a children tree manually. Most authors should -// use `children()` from solid-js, which memoizes the resolved value. -function renderChildren(value: unknown): unknown { - return flatten(value, { skipNonRendered: true }); -} -``` - ## Learn more - [Async reactivity](/concepts/async-reactivity) diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/attribution.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/attribution.mdx index b6ea0ec63..bc851b3d7 100644 --- a/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/attribution.mdx +++ b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/attribution.mdx @@ -512,8 +512,11 @@ not cry wolf). `false` disables. - **Type:** `number | false` Wide-scope warning: emit a diagnostic when a scope's dependency count -reaches this (default 30) — the coarse-read / helper-leak signature. -Re-warns only if the count then grows by another 50%. `false` disables. +reaches this (default 200) — the coarse-read / helper-leak signature. +Re-warns only if the count then grows by another 50%. HMR plumbing is +not counted, and in dev builds the renderer's insert child-resolution +pass (which must track every row's resolved child) is not judged; +HUGE_FAN_IN (2000, always on) still covers both. `false` disables. #### `hotTime` @@ -559,8 +562,8 @@ reaches a node with at least this many subscribers (default 250). The same code the always-on core check emits from GRAPH_SIZE_WARN_AT (2000) up, with `data.write` naming the invalidation; the engine hands over to the core there, so one change never carries two findings. Once per -node, re-warning once the count has grown by another 500. `false` -leaves only the always-on threshold. +node, re-warning once the count has grown by another 500. HMR plumbing +subscribers are not counted. `false` leaves only the always-on threshold. #### `waterfalls` @@ -620,9 +623,13 @@ long hold stays one SILENT_HOLD with the boundary repair appended. Graph-growth warning: emit GRAPH_GROWTH when the live owner count at the settle of the same route has climbed on `visits` consecutive visits -(default 3) to `ratio` or more of the first (default 1.25) — a root or a +(default 3) to `ratio` or more of the first (default 1.25), and so has +the part of it that outlived each of those navigations — a root or a subscription each visit leaves behind, the leak class a heap snapshot -finds. The count is a walk at settle, never per node. `false` disables. +finds, not content a visit owns and leaving disposes. The count is a walk +when a navigation opens and at its settle, never per node. Navigations +that stay on a route pattern continue its visit and are not compared. +`false` disables. #### `abandonedFlights` @@ -632,7 +639,9 @@ Abandoned-flights warning: emit ABANDONED_FLIGHTS when one async source abandons `count` flights within `windowMs` — each superseded by the next before it landed (default 3 / 1000ms: the request-per-keystroke signature, every input asking again and the answers discarded). Once -per window per source. `false` disables. +per window per source. In dev builds a framework node that supersedes +its own flights by design (a frames slot arg re-read by each re-shipped +record) is not judged. `false` disables. #### `fallbackFlashes` @@ -1819,7 +1828,9 @@ INP counts as input delay. Present only when `at` predates the frame. - **Type:** `number` -Wall time of the handler itself, entry to return. +Wall time of the handler itself, entry to return — for the frames of one +event (`InteractionRef.event`), the first listener's entry to the last +one's return, as Event Timing measures processing. #### `writes` @@ -1874,7 +1885,8 @@ waited in — whichever came last. `undefined` while unsettled. - **Type:** `number` The handler returned a thenable (`async () => { await save(); … }`) and -the record waited for it: handler return → the promise settling, in +the record waited for it: handler return → the promise settling (the last +one, when several listeners of one event returned one), in milliseconds, capped at `ASYNC_HANDLER_CAP_MS`. The continuation runs with no frame on the stack, so writes it makes are not attributed to this interaction — only its duration is. Absent when the handler @@ -1897,6 +1909,7 @@ interface InteractionRef { type: string; target?: string; at?: number; + event?: object; } ``` @@ -1921,6 +1934,23 @@ record's `target` is its own string, this one is never mutated). Dispatch time on the `performance.now()` clock; defaults to now. +#### `event` + +- **Type:** `object` + +The dispatch this frame handles — the DOM `Event` itself. Frames with the +same `event` are one interaction: the first opens the record (its `type`, +`target` and `at` describe it) and every later one re-enters it, so each +listener of one dispatch adds its work to one record. Such a record stays +open for joining until the next task — every listener of a dispatch runs +within its task — and is recorded no earlier; a frame for the event after +that opens a new interaction. Without `event` the record is the frame's +alone and can settle as it closes. + +A join key only, compared by identity and held weakly: it is never +recorded — no `InteractionEvent`, origin or other record carries it — and +the engine keeps no strong reference to it. + ### `NavigationEvent` ```ts diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dev.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dev.mdx index e24973195..7c43466c0 100644 --- a/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dev.mdx +++ b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dev.mdx @@ -169,6 +169,7 @@ type DiagnosticCode = | "SETTLE_WALK_UNINITIALIZED_SOURCE" | "FLUSH_IN_EFFECT_CALLBACK" | "PRIMITIVE_IN_FORBIDDEN_SCOPE" + | "PRIMITIVE_IN_EFFECT_CALLBACK" | "NO_OWNER_EFFECT" | "NO_OWNER_BOUNDARY" | "ASYNC_OUTSIDE_LOADING_BOUNDARY" @@ -211,6 +212,7 @@ type DiagnosticCode = | "SERVER_ERROR_SANITIZED" | "SSR_BOUNDARY_WATERFALL" | "SSR_UNDECLARED_LIVE_SOURCE" + | "SSR_LIVE_HOLE_LATCHED" | "SERVER_WRITE" | "REVEAL_IN_RENDER_TO_STRING" | "LAZY_ASSET_UNMAPPED" @@ -218,6 +220,7 @@ type DiagnosticCode = | "HEAD_TAG_INVALID" | "UNRECOGNIZED_INSERT_VALUE" | "UNSCOPED_HOLE_ALLOCATED_IDS" + | "LOWERCASE_EVENT_ATTRIBUTE" | "BINDING_SLOT_POSITION" | "FRAME_MARKER_CORRUPTED" | "DYNAMIC_ASYNC_COMPONENT"; diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dispatch-as-interaction.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dispatch-as-interaction.mdx new file mode 100644 index 000000000..2a6a9aeff --- /dev/null +++ b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/dispatch-as-interaction.mdx @@ -0,0 +1,68 @@ +--- +title: "dispatchAsInteraction" +category: "Advanced / Diagnostics & Dev Hooks" +use_cases: "advanced diagnostics & dev hooks api, dispatchasinteraction usage" +tags: + - "dispatch" + - "as" + - "interaction" + - "advanced" + - "diagnostics" + - "dev" + - "hooks" + - "reference" + - "api" + - "v2" +version: "2.0" +description: "Runs `fn` as a handler of the DOM event `e`, inside the interaction frame the runtime opens for `e` — the one its own listeners for `e` run in." +source_repo: "solidjs/solid" +source_ref: "next" +source_path: "packages/web/src/client.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Runs `fn` as a handler of the DOM event `e`, inside the interaction frame +the runtime opens for `e` — the one its own listeners for `e` run in. A +listener the runtime did not attach (a router's `document` click handler, +analytics, drag-and-drop) wraps its body in this, so its work joins the +same interaction record as the component handlers for the same event +instead of running outside one. In production builds this is `fn()`. + +Use it for listeners that respond to the user (`click`, `submit`, +`keydown`), not for passive ones (`mousemove`, `focusin` preloads), which +would each record an interaction. + +## Import + +```ts +import { dispatchAsInteraction } from "@solidjs/web"; +``` + +## Type signature + +```ts +function dispatchAsInteraction(e: Event, fn: () => T): T; +``` + +## Parameters + +### `e` + +- **Type:** `Event` + +### `fn` + +- **Type:** `() => T` + +## Examples + +```ts +document.addEventListener("click", (e) => + dispatchAsInteraction(e, () => handleAnchorClick(e)) +); +``` + +## Learn more + +- [Debugging reactivity](/guides/debugging-reactivity) diff --git a/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/observe.mdx b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/observe.mdx index 1a15f0ccb..df1466292 100644 --- a/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/observe.mdx +++ b/src/routes/reference/(1)solid-js/(6)advanced/(7)diagnostics-dev-hooks/observe.mdx @@ -552,6 +552,7 @@ type DiagnosticCode = | "SETTLE_WALK_UNINITIALIZED_SOURCE" | "FLUSH_IN_EFFECT_CALLBACK" | "PRIMITIVE_IN_FORBIDDEN_SCOPE" + | "PRIMITIVE_IN_EFFECT_CALLBACK" | "NO_OWNER_EFFECT" | "NO_OWNER_BOUNDARY" | "ASYNC_OUTSIDE_LOADING_BOUNDARY" @@ -594,6 +595,7 @@ type DiagnosticCode = | "SERVER_ERROR_SANITIZED" | "SSR_BOUNDARY_WATERFALL" | "SSR_UNDECLARED_LIVE_SOURCE" + | "SSR_LIVE_HOLE_LATCHED" | "SERVER_WRITE" | "REVEAL_IN_RENDER_TO_STRING" | "LAZY_ASSET_UNMAPPED" @@ -601,6 +603,7 @@ type DiagnosticCode = | "HEAD_TAG_INVALID" | "UNRECOGNIZED_INSERT_VALUE" | "UNSCOPED_HOLE_ALLOCATED_IDS" + | "LOWERCASE_EVENT_ATTRIBUTE" | "BINDING_SLOT_POSITION" | "FRAME_MARKER_CORRUPTED" | "DYNAMIC_ASYNC_COMPONENT"; @@ -1460,8 +1463,10 @@ the boundaries are not measured and this is `0`. `"complete"` — the render ran to its end; `"abandoned"` — the consumer left mid-stream (the sink threw, the readable was cancelled — the `SSR_STREAM_ABANDONED` finding is that request's account) and the -render was torn down; `"error"` — the render failed: a string render -threw, a stream's uncontained failure wound it down through `onError`. +render was torn down, or `createSSRResponse` discarded the page for a +pre-flush redirect (no finding); `"error"` — the render failed: a +string render threw, a stream's uncontained failure wound it down +through `onError`. #### `route` diff --git a/src/routes/reference/(1)solid-js/(7)types/store-types.mdx b/src/routes/reference/(1)solid-js/(7)types/store-types.mdx index 2afdfdc49..174899ae3 100644 --- a/src/routes/reference/(1)solid-js/(7)types/store-types.mdx +++ b/src/routes/reference/(1)solid-js/(7)types/store-types.mdx @@ -13,7 +13,7 @@ version: "2.0" description: "A reactive view of a store's value. Update it through the paired `StoreSetter`." source_repo: "solidjs/solid" source_ref: "next" -source_path: "packages/signals/src/store/store.ts" +source_path: "packages/signals/src/store/types.ts" --- {/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} diff --git a/src/routes/reference/(2)solid-web/(3)components/dynamic.mdx b/src/routes/reference/(2)solid-web/(3)components/dynamic.mdx index c2125b39c..7a8140f83 100644 --- a/src/routes/reference/(2)solid-web/(3)components/dynamic.mdx +++ b/src/routes/reference/(2)solid-web/(3)components/dynamic.mdx @@ -1,9 +1,10 @@ --- -title: "dynamic" +title: "dynamic / dynamicComponent" category: "Components" use_cases: "components api, dynamic usage" tags: - "dynamic" + - "component" - "components" - "reference" - "api" @@ -25,9 +26,12 @@ Creates a stable component whose rendered component comes from a reactive source ```ts import { dynamic } from "@solidjs/web"; +import { dynamicComponent } from "solid-js"; ``` -## Type signature +## `dynamic` + +### Type signature ```ts function dynamic( @@ -36,26 +40,48 @@ function dynamic( ): Component>; ``` -## Parameters +### Parameters -### `source` +#### `source` - **Type:** `() => T | Promise | AsyncIterable | null | undefined | false` Reactive function that returns the component to render: a component function, an intrinsic element name, a promise of either, or a falsy value to render nothing. -### `options` +#### `options` - **Type:** `DynamicOptions` - Optional `deferStream` holds the SSR shell until the source settles. -## Return value +### Return value A stable component. Render it as `` and pass the target component's props to it. -## Examples +### Examples + +```tsx +// `source` can return either a custom Component or a native tag +// name — they're interchangeable, and the returned reference is a +// stable Component you can use anywhere a normal one would go. +const Field = dynamic(() => (multiline() ? RichTextEditor : "input")); +return ; + +// An ambiguous tag inside an SVG tree: say which namespace you mean. +const Link = dynamic(() => "a"); + + + … + +; + +// A polymorphic component: no memo when `as` arrived as a literal. +function Polymorphic(props) { + const Tag = dynamic(() => props.as, { static: isStatic(props, "as") }); + return ; +} +``` ```tsx const CurrentPage = dynamic(() => pages[route()]); @@ -63,10 +89,63 @@ const CurrentPage = dynamic(() => pages[route()]); return ; ``` -## Caveats +### Caveats - The returned component is stable; render it once and let `source` change. Creating it inside the render path recreates the subtree. +## `dynamicComponent` + +`dynamic` for a source that only ever answers with a component — never a +tag name. A reactive, optionally async source; a stable `Component` back; +`{ static: true }` and `{ deferStream: true }`. The source's type excludes +strings, so a tag name is a compile error here — use `@solidjs/web`'s +`dynamic` for one. + +Cost model, which is the reason this exists: `dynamic` must be able to +render a tag, so one `dynamic` anywhere on a page retains the element +runtime — `createElement`, `spread` and the prop-collection helpers, the +SVG/MathML tables — for everyone, whether or not any source ever answers +with a string (a bundler cannot know what a source will resolve to). +`dynamicComponent` has no tag arm and never references that runtime, so a +page whose only dynamic mounts are components pays nothing for it. + +This is the documented way to mount a server component: a server +function's answer is a component reference, and the frames transport +resolves every call for the same function to the same mount identity, so +a refetch or an argument change is delivered into the mounted instance +rather than remounting it. + +### Type signature + +```ts +const dynamicComponent: >( + source: () => C | Promise | AsyncIterable | null | undefined | false, + options?: DynamicOptions +) => Component>; +``` + +### Parameters + +#### `source` + +- **Type:** `() => C | Promise | AsyncIterable | null | undefined | false` + +#### `options` + +- **Type:** `DynamicOptions` +- Optional + +### Examples + +```tsx +const Story = dynamicComponent(() => getStory(props.storyId)); +return {p.children}} />; + +// A client component chosen by (sync) data — the same thing `dynamic` +// does, without retaining the element runtime for the page. +const Page = dynamicComponent(() => (page().editable ? Editor : Viewer)); +``` + ## Learn more - [Dynamic components](/concepts/components-and-jsx#dynamic-components) @@ -76,6 +155,9 @@ return ; ### `DynamicOptions` +Options for `dynamicComponent` (and for `@solidjs/web`'s `dynamic`, which +passes them through to the same core). + ```ts interface DynamicOptions { deferStream?: boolean; @@ -90,12 +172,13 @@ interface DynamicOptions { SSR only: hold the document's first flush until the source settles, so the resolved component renders into the shell instead of streaming in behind its boundary's fallback. Same meaning as `createMemo`'s -`deferStream`. Ignored on the client. +`deferStream`. Default `false` — a source is data of unknown cost and +streams by default. Ignored on the client. #### `static` - **Type:** `boolean` The source cannot change: call it once, untracked, now, and render the -result with no computation per instance (see `dynamic`). The source must -resolve synchronously. +result with no computation per instance. The source must resolve +synchronously. diff --git a/src/routes/reference/(2)solid-web/(5)server-functions/configure-client.mdx b/src/routes/reference/(2)solid-web/(5)server-functions/configure-client.mdx index befb1d66c..a716e8667 100644 --- a/src/routes/reference/(2)solid-web/(5)server-functions/configure-client.mdx +++ b/src/routes/reference/(2)solid-web/(5)server-functions/configure-client.mdx @@ -118,13 +118,26 @@ interface ServerFunctionsClientConfig { capture?(info: { id: string; meta: unknown }): unknown; handle( response: Response, - ctx: { id: string; meta: unknown; args: unknown[]; context: unknown } + ctx: { + id: string; + meta: unknown; + args: unknown[]; + context: unknown; + retry(): Promise; + } ): unknown; + intercept?(info: { + id: string; + meta: unknown; + args: unknown[]; + retry(): Promise; + }): unknown; resume?(info: { id: string; meta: unknown; args: unknown[]; }): { position?: string; headers?: Record } | undefined; + onLive?(): void; }; serializeArgs?(args: unknown[]): string | Promise; } @@ -212,9 +225,13 @@ configureServerFunctionsClient({ }); ``` +Return synchronously when you can: hydration adopts a server-rendered +read only if its request is sent in the same tick, so an async hook +makes every hydrated read fetch again from the browser. + #### `responseHandler` -- **Type:** `{ capture?(info: { id: string; meta: unknown }): unknown; handle( response: Response, ctx: { id: string; meta: unknown; args: unknown[]; context: unknown } ): unknown; resume?(info: { id: string; meta: unknown; args: unknown[]; }): { position?: string; headers?: Record } | undefined; }` +- **Type:** `{ capture?(info: { id: string; meta: unknown }): unknown; handle( response: Response, ctx: { id: string; meta: unknown; args: unknown[]; context: unknown; retry(): Promise; } ): unknown; intercept?(info: { id: string; meta: unknown; args: unknown[]; retry(): Promise; }): unknown; resume?(info: { id: string; meta: unknown; args: unknown[]; }): { position?: string; headers?: Record } | undefined; onLive?(): void; }` Response-side integration seam — the client mirror of the handler's `transformResult`. `handle(response, ctx)` sees every response before diff --git a/src/routes/reference/(2)solid-web/(6)request-response/configure-server-errors.mdx b/src/routes/reference/(2)solid-web/(6)request-response/configure-server-errors.mdx index 95c3f888e..3e43e129d 100644 --- a/src/routes/reference/(2)solid-web/(6)request-response/configure-server-errors.mdx +++ b/src/routes/reference/(2)solid-web/(6)request-response/configure-server-errors.mdx @@ -1,12 +1,14 @@ --- -title: "configureServerErrors" +title: "configureServerErrors / reportRequestFailure" category: "Request & response" use_cases: "request & response api, configureservererrors usage" tags: - "configure" - "server" - "errors" + - "report" - "request" + - "failure" - "response" - "reference" - "api" @@ -25,21 +27,45 @@ Registers the ambient server error hook: called once per error object for every ## Import ```ts -import { configureServerErrors } from "@solidjs/web"; +import { configureServerErrors, reportRequestFailure } from "@solidjs/web"; ``` -## Type signature +## `configureServerErrors` + +### Type signature ```ts function configureServerErrors(config: ServerErrorsConfig): void; ``` -## Parameters +### Parameters -### `config` +#### `config` - **Type:** `ServerErrorsConfig` +## `reportRequestFailure` + +Reports a failure that fails a request outside any render (see the +server entry's `reportRequestFailure`). Server-only; on the client this is +a no-op. + +### Type signature + +```ts +function reportRequestFailure(error: unknown, event: RequestEvent): void; +``` + +### Parameters + +#### `error` + +- **Type:** `unknown` + +#### `event` + +- **Type:** `RequestEvent` + ## Learn more - [Hear every failure the server handles](/guides/observability#hear-every-failure-the-server-handles) @@ -54,7 +80,7 @@ function configureServerErrors(config: ServerErrorsConfig): void; Where a server-side failure was met, as the server error hook hears it — the client's view of the server entry's type (see `configureServerErrors` there). `kind: "render"`: `fallback`/`client`/`failed`; `kind: -"server-function"`: `thrown`/`channel`. +"server-function"`: `thrown`/`channel`; `kind: "request"`: `failed`. ```ts interface ServerErrorContext extends Omit { @@ -108,7 +134,7 @@ Where a server failure was met, as the server error hook hears it (see `@solidjs ```ts interface ServerErrorSite { - kind: "render" | "server-function"; + kind: "render" | "server-function" | "request"; handling: "fallback" | "client" | "failed" | "serialize" | "thrown" | "channel"; boundary?: string; @@ -122,7 +148,7 @@ interface ServerErrorSite { #### `kind` -- **Type:** `"render" | "server-function"` +- **Type:** `"render" | "server-function" | "request"` #### `handling` diff --git a/src/routes/reference/(2)solid-web/(6)request-response/respond.mdx b/src/routes/reference/(2)solid-web/(6)request-response/respond.mdx index 6656ae860..2f75af0ef 100644 --- a/src/routes/reference/(2)solid-web/(6)request-response/respond.mdx +++ b/src/routes/reference/(2)solid-web/(6)request-response/respond.mdx @@ -24,7 +24,13 @@ A value paired with response metadata (status, headers, `revalidate`) — for the things a naked return can't express. Progressive enhancement stays invisible: the carried response holds a plain JSON body so consumers without the client runtime (no-JS form posts, direct HTTP) -get real JSON, while integrations read `value` — no reparse. +get real JSON, while integrations read `value` — no reparse. The body is +encoded when read, so `value` may be anything a plain return may. + +Typed as `value`'s type: every caller of the function returning it — +over HTTP or in-process — receives the value, never the envelope. At +runtime it is a `ResponseEnvelope`, a real `Response` that integrations +recognize with `isResponseEnvelope()`. ## Import @@ -97,8 +103,14 @@ payloads); the HTTP handler forwards `response`'s headers and (non-redirect) status and encodes `value` as the body through the codec, while client-only integrations read `value` directly — no reparse. +The envelope IS a `Response` — the given response's body, status and +headers — so a consumer that knows nothing of Solid (a filesystem +router's API dispatch, any fetch-style handler) answers with it as-is. +`response` is the envelope itself, or `undefined` when constructed +without one. + ```ts -interface ResponseEnvelope { +interface ResponseEnvelope extends Response { response: Response | undefined; value: T; } diff --git a/src/routes/reference/(2)solid-web/(7)performance-tracks/enable-performance-tracks.mdx b/src/routes/reference/(2)solid-web/(7)performance-tracks/enable-performance-tracks.mdx index bee67b384..4a3871836 100644 --- a/src/routes/reference/(2)solid-web/(7)performance-tracks/enable-performance-tracks.mdx +++ b/src/routes/reference/(2)solid-web/(7)performance-tracks/enable-performance-tracks.mdx @@ -93,12 +93,14 @@ interface PerformanceTracksOptions { - **Type:** `AttributionOptions` Options for the engine hold this adapter takes -(`attribution.enable({ log: false, ...attribution })`). The adapter asks -for no console log — the timeline is the output — which, the engine -combining holds by the most demanding request, quiets the console only -while nobody else wants it: a console session beside the tracks keeps -its log. `checks` is left to the engine's default; pass `checks: false` -for records only (honoured while no other holder wants the checks). +(`attribution.enable({ log: false, checks: false, ...attribution })`). +The adapter asks for no console log and records only — the timeline is +the output — which, the engine combining holds by the most demanding +request, quiets the console and the cost checks (`hotRuns`, `hotTime`, +`wideDeps`, `unstableMemos`, `fanOut`, `wastedRecompute`) only while +nobody else wants them: a console session beside the tracks keeps its +log and its checks. Pass `checks: true` to have the tracks' own hold +run them, their findings painted as markers like any other. #### `minMs` From 6780a9d00afedb5a0dae16fd7ba6d2cb443caf62 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 01:08:35 +0000 Subject: [PATCH 2/3] docs: catch up hand-written pages with solid rc.14-next and router next.34-next.40 Solid: seedless derived stores and complete seeds (#4073); storePath recipes become draft setters (#4053); respond() envelope and non-JSON bodies; performance-tracks defaults; kind "request" and reportRequestFailure; lowercase on* migration note; dynamicComponent, lazyModule and onHydrationEnd mentions. Router: link preloading is opt-in (preloadLinks strategies); data-pending needs pendingLinks; aria-current matches path and query; decoded params (#681); useInRouter (#675); action settle timing; liveQuery reference; anchor attributes typed by the router. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y4HXPRqXPnqriNxDyin6eK --- .../(2)concepts/(1)components-and-jsx.mdx | 4 +- src/routes/(2)concepts/(2)stores.mdx | 26 ++-- .../(3)mutations-and-responses.mdx | 10 +- .../(6)middleware-and-api-routes.mdx | 8 +- .../(4)routing/(1)solid-router/(0)index.mdx | 6 +- .../(4)routing/(1)solid-router/(1)setup.mdx | 3 +- .../(1)solid-router/(2)route-definitions.mdx | 4 +- .../(1)solid-router/(4)navigation.mdx | 37 ++++-- .../(4)routing/(1)solid-router/(5)data.mdx | 37 ++++-- src/routes/(5)guides/(11)performance.mdx | 14 +- src/routes/(5)guides/(12)typescript.mdx | 4 +- src/routes/(5)guides/(14)testing.mdx | 19 +++ src/routes/(5)guides/(15)observability.mdx | 20 +-- src/routes/(5)guides/(7)protected-routes.mdx | 4 +- src/routes/(6)migration/(0)from-solid-1.mdx | 24 +++- .../(6)migration/(3)from-solid-router.mdx | 38 +++++- src/routes/(7)glossary.mdx | 2 + .../reference/(3)solid-router/(0)index.mdx | 4 +- .../(3)solid-router/(1)router-factory.mdx | 123 +++++++++++++++++- .../(3)solid-router/(2)routes-and-paths.mdx | 18 +++ .../(3)solid-router/(3)navigation.mdx | 25 +++- .../reference/(3)solid-router/(4)data.mdx | 54 +++++++- .../reference/(3)solid-router/(8)types.mdx | 4 + 23 files changed, 408 insertions(+), 80 deletions(-) diff --git a/src/routes/(2)concepts/(1)components-and-jsx.mdx b/src/routes/(2)concepts/(1)components-and-jsx.mdx index 038691c4c..90980fb43 100644 --- a/src/routes/(2)concepts/(1)components-and-jsx.mdx +++ b/src/routes/(2)concepts/(1)components-and-jsx.mdx @@ -504,7 +504,9 @@ export function Preview() { ``` The source may also resolve to a native tag name or an async component. -See the [`dynamic()` reference](/reference/solid-web/components/dynamic). +Because a source may answer with a tag name, one `dynamic` on a page keeps the element runtime in the bundle. +When the source only ever answers with a component, as `Result` does, `dynamicComponent` from `solid-js` takes the same source and options without that cost; a tag name is a type error there. +See the [`dynamic()` reference](/reference/solid-web/components/dynamic), which covers both. ## Try it: remove a line from the cart diff --git a/src/routes/(2)concepts/(2)stores.mdx b/src/routes/(2)concepts/(2)stores.mdx index 8df4e501a..e5f237c61 100644 --- a/src/routes/(2)concepts/(2)stores.mdx +++ b/src/routes/(2)concepts/(2)stores.mdx @@ -150,8 +150,9 @@ Store writes are staged and applied in the same batch as signal writes, so a rea [When updates land](/concepts/reactivity#when-updates-land) explains the batch and when to call `flush()`. :::note[Path setters from Solid 1] -[`storePath`](/reference/solid-js/advanced/store-advanced/store-path) accepts the path-and-value form Solid 1 used, `setCart(storePath("coupon", "SAVE10"))`, for code that has not moved yet. -New code uses the draft. +The setter takes no path arguments. +A Solid 1 path setter such as `setCart("coupon", "SAVE10")` becomes a draft write to the same path, `setCart((draft) => { draft.coupon = "SAVE10"; })`. +Ranges and filters become ordinary loops over the draft, and setting a key to `undefined` to remove it becomes `delete`. ::: ## Derive a store with a projection @@ -160,7 +161,8 @@ A projection is a store whose value is computed from other reactive values. Where a memo derives one value, a projection derives an object or array whose properties are tracked separately, and whose items keep their identity from one computation to the next. [`createProjection`](/reference/solid-js/stores/create-projection) takes a function and a seed. -The function receives a draft of the seed and may mutate it or return a replacement; a returned array is reconciled into the store by `id`: +The function receives a draft of the seed and may mutate it or return a replacement; a returned array is reconciled into the store by `id`. +The seed is a complete value of the store's type, and an array projection always takes one, `[]`: ```tsx import { For, createProjection, createSignal, createStore } from "solid-js"; @@ -213,23 +215,23 @@ The projection function may return a promise. The function form of `createStore` is a projection with a setter, and it is the normal way to load server data into a store: ```ts -const [cart, setCart] = createStore(async () => api.cart(), { - items: [] as CartItem[], -}); +const [cart, setCart] = createStore(async () => api.cart()); ``` The request starts when the store is created, and again whenever a reactive value the function read changes. Each response reconciles into the same proxy by `id`, so items the server did not change keep their identity and their DOM. A [`Loading`](/concepts/boundaries) boundary shows a fallback before the first response, [`isPending(() => cart.items)`](/reference/solid-js/reactivity/is-pending) reports a refetch, and [`refresh(cart)`](/reference/solid-js/lifecycle-actions/refresh) asks the server again. -With an async function, the seed is not shown as a first answer by default; readers wait for the first response the same way they wait for an async memo. -[Async reactivity](/concepts/async-reactivity) explains that wait, and the `seedLoadingValue` option that makes the seed an acceptable first answer. +This form takes no seed: the function reads no draft, and each response is a complete cart. +Readers wait for the first response the same way they wait for an async memo. +A store with an object root may omit the seed this way; an array root still takes `[]`, and a seed, when given, is a complete value of the store's type, not a partial one. +[Async reactivity](/concepts/async-reactivity) explains that wait, and the `seedLoadingValue` option that makes a seed an acceptable first answer; that option needs a seed. Split the request into its own memo only when a second reader shapes the same response differently: ```ts const response = createMemo(() => api.cart()); -const [cart] = createStore(() => response(), { items: [] as CartItem[] }); +const [cart] = createStore(() => response()); const itemCount = createMemo(() => response().items.length); ``` @@ -243,9 +245,7 @@ It shows immediately, and when the action settles Solid removes it and shows the ```ts import { action, createOptimisticStore, refresh } from "solid-js"; -const [cart, setCart] = createOptimisticStore(async () => api.cart(), { - items: [] as CartItem[], -}); +const [cart, setCart] = createOptimisticStore(async () => api.cart()); const setQuantity = action(function* (id: string, quantity: number) { setCart((draft) => { @@ -293,7 +293,7 @@ Reassign the property to a new instance through the setter to notify readers, or - Change state on the setter's draft; a write to the proxy itself is ignored. - Assign the one property that changed rather than rebuilding the collection, so rows keep their identity. - Derive a collection with `createProjection`, which reconciles results by `id`; derive a scalar with `createMemo`. -- Load server data with `createStore(async () => ..., seed)`; each response reconciles into the same proxy. +- Load server data with `createStore(async () => ...)`; each response reconciles into the same proxy, and an array root takes a `[]` seed. - Make optimistic writes inside an `action` on a `createOptimisticStore`, and `refresh` the source when the request settles. ## Next steps diff --git a/src/routes/(3)building-apps/(3)server-functions/(3)mutations-and-responses.mdx b/src/routes/(3)building-apps/(3)server-functions/(3)mutations-and-responses.mdx index fe07c7fb3..e107017d2 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(3)mutations-and-responses.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(3)mutations-and-responses.mdx @@ -41,10 +41,13 @@ export async function createProduct(input: CreateProductInput) { ``` Call `createProduct(input)` from the admin page and the promise resolves to `product`, not to the envelope. -The transport applies the 201 and the header to the HTTP response, and a plain HTTP caller such as `curl` receives a JSON body with the same product in it. +`respond()` is typed as `T`, so the function's inferred return type is the product's, and a direct call made on the server during SSR unwraps the envelope the same way. +The transport applies the 201 and the header to the HTTP response. +A plain HTTP caller such as `curl` receives a JSON body with the same product in it when JSON carries the value faithfully; a value JSON cannot represent, such as a `bigint`, is encoded the same way as a plain return. :::deep-dive[The envelope on the wire] -`respond()` returns a `ResponseEnvelope`, an object holding the `Response` that carries the metadata and the `value` the caller should see. +At runtime `respond()` returns a `ResponseEnvelope`, a `Response` subclass that carries the status, headers, and body, plus the `value` the caller should see. +Because it is a real `Response`, a fetch-style handler with no Solid knowledge can answer with it as it is. The server-function handler forwards the response's status and headers and encodes the value as the body. Integration code that has to recognize an envelope, such as a router applying revalidation, uses [`isResponseEnvelope()`](/reference/solid-web/request-response/respond); the check is a registered symbol, so it works when a bundle contains two copies of the runtime. Application code never touches the envelope directly. @@ -52,7 +55,8 @@ Application code never touches the envelope directly. ## Redirect the caller -`redirect()` returns a `Response` with a `Location` header: +`redirect()` returns a `Response` with a `Location` header. +Its return type defaults to `never`, so a returned redirect drops out of the function's inferred return type; `reload()` is typed the same way: ```ts // src/data/account.ts diff --git a/src/routes/(3)building-apps/(6)middleware-and-api-routes.mdx b/src/routes/(3)building-apps/(6)middleware-and-api-routes.mdx index 8eaff55e1..f7b19fe5c 100644 --- a/src/routes/(3)building-apps/(6)middleware-and-api-routes.mdx +++ b/src/routes/(3)building-apps/(6)middleware-and-api-routes.mdx @@ -147,17 +147,21 @@ Production has no Vite pipeline, so every request that reaches the end of the ch A middleware that wraps `next()` in `try`/`catch` sees anything the rest of the chain throws: ```ts +import { getRequestEvent, reportRequestFailure } from "@solidjs/web"; + async function catchErrors(_request: Request, next: () => Promise) { try { return await next(); } catch (error) { - console.error(error); + reportRequestFailure(error, getRequestEvent()!); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } } ``` -Log the real error and return a generic body. +Report the real error and return a generic body. +`reportRequestFailure` hands the error to the server error hook as `kind: "request"`, the same hook that hears render and server-function failures, and logs it with `console.error` when no hook is registered; [Observability](/guides/observability#hear-every-failure-the-server-handles) sets that hook up. +An error the hook already heard, such as a failed render, is reported once. Exception messages can carry database, filesystem, or token details; the only errors whose text belongs in a response are the ones the app marked safe, which [Mutations and responses](/building-apps/server-functions/mutations-and-responses#handle-thrown-errors) covers for server functions. Production builds also wrap the generated page render in a default error boundary; set `start.errorBoundary: false` when this middleware owns errors, as [App structure](/building-apps/app-structure#common-problems) notes. diff --git a/src/routes/(4)routing/(1)solid-router/(0)index.mdx b/src/routes/(4)routing/(1)solid-router/(0)index.mdx index 3346b33aa..832c9ebb7 100644 --- a/src/routes/(4)routing/(1)solid-router/(0)index.mdx +++ b/src/routes/(4)routing/(1)solid-router/(0)index.mdx @@ -37,7 +37,7 @@ export const { paths } = Router; Because the route tree is a literal, `paths.products()` without an argument is a type error, and so is `paths.produts`. `lazy` splits each page into its own chunk. -The router loads a chunk when a link to it is hovered or when the route is matched, whichever comes first. +The router loads a chunk when the route is matched, or earlier when a [link preload strategy](/routing/solid-router/data#preloading-from-links) warms it. ## Mount it and add a layout @@ -141,10 +141,11 @@ export const getProduct = query(async (id: string) => { ```tsx // src/router.ts import { lazy } from "solid-js"; -import { createRouter } from "@solidjs/router"; +import { createRouter, intentPreload } from "@solidjs/router"; import { getProduct } from "./data/products"; export const Router = createRouter({ + preloadLinks: intentPreload(), routes: [ { path: "/", component: lazy(() => import("./pages/Home")) }, { @@ -179,6 +180,7 @@ export default function Product( } ``` +`preloadLinks: intentPreload()` turns on link preloading, which is off by default. Hover **Featured** and watch the network tab: the product request starts before the click. Click, and the page renders with the data already there. diff --git a/src/routes/(4)routing/(1)solid-router/(1)setup.mdx b/src/routes/(4)routing/(1)solid-router/(1)setup.mdx index 588b46612..6809d3c00 100644 --- a/src/routes/(4)routing/(1)solid-router/(1)setup.mdx +++ b/src/routes/(4)routing/(1)solid-router/(1)setup.mdx @@ -123,7 +123,8 @@ The other options exist for a specific situation each: | `base` | The app is served under a prefix such as `/app`; matching and `paths` both include it. | | `history` | The app is not a normal page: an Electron shell or `file://` needs `hashHistory()`, tests need `memoryHistory()`. | | `preload` | The root layout needs data of its own; it runs once per mount or server request and reaches the function child as `props.data`. | -| `preloadLinks` | Hover and focus preloading costs more than it saves; set `false` and use `preload="false"` per link for finer control. | +| `preloadLinks` | Links should warm route code and data before the click; pass `intentPreload()` or another strategy (off by default). | +| `links` | Links should get `data-pending` during an in-flight navigation; pass `pendingLinks`. | | `explicitLinks` | Some anchors inside the router must stay as full page loads; only anchors with a `link` attribute are then handled by the router. | | `scrollRestoration` | The app manages scroll itself; set `false` to stop the router restoring the position on back and forward (on by default). | | `transformUrl` | Incoming pathnames need rewriting before matching, for example to strip a locale prefix. | diff --git a/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx b/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx index b9782bc49..8f041e86f 100644 --- a/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx +++ b/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx @@ -131,6 +131,8 @@ const orderRoute = defineRoute({ `/account/orders/42` matches; `/account/orders/latest` falls through to the next route that matches, usually the `*404` catch-all. +Filters test the decoded parameter value, so `{ tag: ["café"] }` matches `/tags/caf%C3%A9`. + The built-in `int` filter accepts integer strings at runtime. It also changes the corresponding `Router.paths` argument to `number`, so `paths.account.orders(42)` type-checks and `paths.account.orders("latest")` does not. @@ -193,7 +195,7 @@ export default defineRoutes([ ``` Visit `/` and the admin route table is not downloaded. -Visit `/admin/products/mug`, or hover a link to it, and the thunk runs, the router adds the resolved subtree to the compiled route tree, and the match completes. +Visit `/admin/products/mug`, or preload a link to it with a [link preload strategy](/routing/solid-router/data#preloading-from-links), and the thunk runs, the router adds the resolved subtree to the compiled route tree, and the match completes. TypeScript infers `paths.admin.products("mug")` through the import's promise type when the imported route array retains literal types. Use a deterministic thunk such as `() => import("./admin/routes")`. diff --git a/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx b/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx index 031a0dadb..1830d4441 100644 --- a/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx +++ b/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx @@ -32,7 +32,8 @@ A few attributes adjust how the router treats one link: - `replace` replaces the current history entry instead of pushing. - `noscroll` keeps the scroll position after navigation. - `state` supplies a JSON value for `location.state` on the destination. -- `preload="false"` skips the route-data preload on hover for this link; the code chunk still warms. +- `preload="false"` opts the link out of every [preload strategy](/routing/solid-router/data#preloading-from-links), for both route code and data. +- `preload="viewport"` and `preload="eager"` mark the link for the `viewportPreload()` and `eagerPreload()` strategies. - `link` marks an anchor for the router when `explicitLinks: true` is set, in apps where most anchors should stay full page loads. ## Build URLs with `paths` @@ -129,6 +130,10 @@ params.id; // string Inside a route component, `props.params` is the same object, already typed when the component is declared with `RouteProps`. +Parameter values are decoded: on `/products/caf%C3%A9`, `params.id` is `"café"`. +An encoded slash stays inside its parameter, so `%2F` in one segment reads as `/` in one value. +`location.pathname` keeps the URL's encoding. + ## Type search parameters Search parameters are the right home for state that should survive a refresh and be shareable: a filter, a sort order, a page number. @@ -184,14 +189,23 @@ Keep search schemas to synchronous parsing and coercion; a value that needs a se ## Show active and pending links Click a link to a page whose data takes a moment. -The current page stays on screen, and the clicked link gets a `data-pending` attribute until the destination is ready. -That is the visible half of the held update described in [Async reactivity](/concepts/async-reactivity#settled-view-and-in-flight-work), and it is why an app without any loading spinner still feels responsive: the link itself shows that something is happening. +The current page stays on screen until the destination is ready. +That is the held update described in [Async reactivity](/concepts/async-reactivity#settled-view-and-in-flight-work), and marking the clicked link is what makes an app without any loading spinner still feel responsive. + +The router sets two attributes on the anchors it handles: + +- `aria-current="page"` when the path and the query match, ignoring parameter order and the hash. + On `/?filter=active`, a link to `/?filter=active` is current and a link to `/` is not. +- `data-active` on an exact or descendant path match, ignoring the query, so the Account link is active on `/account/orders/42`. -The router sets three attributes on the anchors it handles: +A third, `data-pending` on the target of an in-flight link click or `navigate()` call, needs the `pendingLinks` plugin: -- `aria-current="page"` on an exact match. -- `data-active` on an exact or descendant match, so the Account link is active on `/account/orders/42`. -- `data-pending` on the target of an in-flight navigation. +```ts +// src/router.ts +import { createRouter, pendingLinks } from "@solidjs/router"; + +export const Router = createRouter({ routes, links: pendingLinks }); +``` Style them in CSS with no component code: @@ -211,8 +225,9 @@ a[data-pending] { ``` The root path `/` is active only on an exact match; otherwise it would be active everywhere. +An `aria-current` value you set yourself, such as `aria-current="step"`, is left alone. -For a component that is not an anchor, or an anchor that needs the state in JSX, `useLinkState` returns the same three as accessors: +For a component that is not an anchor, or an anchor that needs the state in JSX, `useLinkState` returns the same three as accessors, and its `pending` works without the plugin: ```tsx import { useLinkState } from "@solidjs/router"; @@ -233,7 +248,7 @@ function Tab(props: { href: string; children: JSX.Element }) { ## Observe and guard navigation `useIsRouting()` is true while a navigation is waiting on route work. -Use it for a top-of-page progress bar; for a single link, `data-pending` is already there. +Use it for a top-of-page progress bar; for a single link, use `data-pending` or `useLinkState().pending`. `useMatch(() => pattern)` tests a pattern against the current pathname without needing a route for it, and `useRouteMatches()` returns the matched route definitions with their `info`, which is how a breadcrumb reads route metadata. @@ -285,12 +300,12 @@ Add the schema with a transform to `Number`, and read with `useSearchParams(Rout - Use `useNavigate` for navigation that is a consequence of something else; return `redirect()` from a mutation when the server knows the destination. - Read `useLocation()` fields individually; each is reactive on its own. - Put shareable state in search parameters, and give the route a synchronous `search` schema to get typed, parsed values. -- Style `aria-current="page"`, `data-active`, and `data-pending` in CSS; the router sets them on the anchors it handles. +- Style `aria-current="page"` and `data-active` in CSS; the router sets them on the anchors it handles. Add `links: pendingLinks` for `data-pending`. - Guard unsaved changes with `useBeforeLeave`, and call `retry(true)` after the user confirms. ## Next steps - [Nested routes and layouts](/routing/solid-router/nested-routes): where section navigation lives and what stays mounted across links. -- [Data loading and mutations](/routing/solid-router/data): what the router preloads on hover, and `redirect()` from an action. +- [Data loading and mutations](/routing/solid-router/data): how links preload ahead of a click, and `redirect()` from an action. - [State management](/guides/state-management#state-in-the-url): which page state belongs in the URL at all. - [Navigation API reference](/reference/solid-router/navigation): signatures for every primitive on this page. diff --git a/src/routes/(4)routing/(1)solid-router/(5)data.mdx b/src/routes/(4)routing/(1)solid-router/(5)data.mdx index 3e8686d11..41b7475f7 100644 --- a/src/routes/(4)routing/(1)solid-router/(5)data.mdx +++ b/src/routes/(4)routing/(1)solid-router/(5)data.mdx @@ -57,7 +57,7 @@ export default function Product( } ``` -Hover a product link and the request appears in the network tab before the click. +With [link preloading](#preloading-from-links) turned on, hover a product link and the request appears in the network tab before the click. Click, and the heading renders with the name already there. ![A timeline from hovering the link, to the click, to the component running. The preload calls the query on hover; the memo in the component reads the same query; both reach one cached request, started at the earliest call.](/images/diagrams/router-three-data-pieces.svg) @@ -84,7 +84,7 @@ The intent tells you why the work started: | `"initial"` | First render, including the server render | | `"navigate"` | A link click or `navigate()` call | | `"native"` | Browser back or forward | -| `"preload"` | The user hovered, focused, or touched a link | +| `"preload"` | A link preload strategy or `usePreloadRoute` | Use it to skip work that is too expensive to run on a hover. @@ -104,11 +104,32 @@ Return a value from `preload` only for something that should not change while th ### Preloading from links -The router starts preloading when a user hovers, focuses, or touches a link, after a short delay. -By default that warms the lazy component chunk and runs the route's `preload`. +Links do not preload until you give the router a strategy with `preloadLinks`. +`intentPreload()` preloads when a user rests the pointer on a link, focuses it, or touches it: + +```ts +// src/router.ts +import { createRouter, intentPreload } from "@solidjs/router"; + +export const Router = createRouter({ routes, preloadLinks: intentPreload() }); +``` + +That warms the lazy component chunk and runs the route's `preload`. The query cache keeps the result for a few seconds so the navigation that follows finds it ready. -Turn data preloading off for one link when it is expensive or has side effects: +The other strategies combine with it in an array: + +| Strategy | Preloads | Runs route `preload` by default | +| ------------------- | ------------------------------------------------------------------------------- | ------------------------------- | +| `intentPreload()` | On hover after a short delay, on focus, and on touch | Yes | +| `tapPreload()` | On `pointerdown`, ahead of the click | Yes | +| `viewportPreload()` | Links marked `preload="viewport"` once they have stayed in view | No | +| `eagerPreload()` | Links marked `preload="eager"` once the page has loaded and the browser is idle | No | + +Pass `{ data: false }` or `{ data: true }` to change the last column. +The [router factory reference](/reference/solid-router/router-factory#link-preload-strategies) lists every option. + +Opt one link out of preloading, chunk and data, when it is expensive or has side effects: ```tsx @@ -116,7 +137,6 @@ Turn data preloading off for one link when it is expensive or has side effects: ``` -Or turn off link preloading altogether, chunk and data, with `preloadLinks: false` in `createRouter`. To warm a route from code, for example when a list scrolls into view: ```tsx @@ -277,7 +297,7 @@ A mutation has three moments the UI may want to reflect: while it runs, when it ### While it runs -The router sets `aria-busy="true"` on the submitting form. +The router sets `aria-busy="true"` on the submitting form until the action's result is on screen, including any revalidation or redirect it triggers. Style it in CSS and no component code is needed: ```css @@ -384,6 +404,7 @@ onKeyDown={(event) => { ``` Everything else is the same: revalidation, submissions, `.onSubmit`. +The promise from `remove()` resolves when the action body finishes, which can be before the revalidated data renders and `.onSettled` runs. What you give up is the no-JavaScript path, which only a form provides. ## What revalidates after a mutation @@ -448,4 +469,4 @@ Wrap it in `markSafeError` to send the message to the browser, or throw the vali - [Server rendering and hydration](/routing/solid-router/server-rendering): hydrating query results and single-flight mutations. - [Data fetching patterns](/guides/data-fetching-patterns): search, pagination, sharing, polling, and failures, with or without the router. - [Protected routes](/guides/protected-routes#redirect-before-render): a `preload` and a `query` that redirect a signed-out visitor before the page paints. -- [Data API reference](/reference/solid-router/data): signatures for `query`, `revalidate`, `action`, `useAction`, and `useSubmissions`. +- [Data API reference](/reference/solid-router/data): signatures for `query`, `liveQuery`, `revalidate`, `action`, `useAction`, and `useSubmissions`. diff --git a/src/routes/(5)guides/(11)performance.mdx b/src/routes/(5)guides/(11)performance.mdx index 88e07bf38..a0a782921 100644 --- a/src/routes/(5)guides/(11)performance.mdx +++ b/src/routes/(5)guides/(11)performance.mdx @@ -37,7 +37,7 @@ console.table(feedback().flights); Name the scopes first: `createMemo(fn, { name: "visible" })`, `createStore(value, { name: "catalog" })`. Every table and every `why(visible)` chain refers to nodes by that name, and an unnamed memo prints as `computed`. -While attribution is enabled, Solid also warns on its own when a scope re-runs 120 or more times in one second (`HOT_SCOPE_RERUNS`), spends 8ms or more of compute in one second (`HOT_SCOPE_TIME`), tracks 30 or more sources (`WIDE_SCOPE_DEPS`), or when one write reaches 250 or more subscribers (`HUGE_FAN_OUT` at the engine's `fanOut` threshold, with `data.write` naming the write). +While attribution is enabled, Solid also warns on its own when a scope re-runs 120 or more times in one second (`HOT_SCOPE_RERUNS`), spends 8ms or more of compute in one second (`HOT_SCOPE_TIME`), tracks 200 or more sources (`WIDE_SCOPE_DEPS`), or when one write reaches 250 or more subscribers (`HUGE_FAN_OUT` at the engine's `fanOut` threshold, with `data.write` naming the write). Two warnings are always on in development: `HUGE_FAN_OUT` from 2,000 subscribers and `HUGE_FAN_IN` from 2,000 sources. [Debugging reactivity](/guides/debugging-reactivity#something-updates-too-often) explains how to read each report. @@ -62,6 +62,7 @@ release(); Record a profile, type one character, and the group shows a wave on **Propagation** labelled by the write and what it reached, `query "m" → "mu" — input on input#search · 2,003 runs, 2,000 unchanged`, with every run inside it beneath, labelled by what made it run. **Effects** and **Memos** carry one span per re-run, colored by self time and marked as a warning when the run changed nothing; **Async** shows each request from kickoff to landing, with abandoned ones as warnings, and each `Loading` fallback from shown to hidden; **Holds** shows each wait labelled by its blockers; **Interactions** shows the keystroke from input delay through the handler to settle; **Navigations** shows each route change; **Server** shows the page's server-function calls and, beneath each, the server's own time for it. Every finding while the tracks are on is also a marker on the panel's Timings track, `HOT_SCOPE_RERUNS — `, and a warning or worse is annotated for the Insights sidebar with a link to the code's repair guide. +The cost findings, such as `HOT_SCOPE_RERUNS`, appear only while some hold runs the cost checks: the console session above does, and the tracks' own hold does not unless asked. A label starts at the nearest component you wrote, ` › visible` rather than the full path from the document root, because the panel elides the middle of a label that does not fit. Solid's own flow components (`Show`, `For`, `Loading`, and the rest) never anchor a label, so a node under one reads ` › › effect`; the unshortened path is the span's `Owner path` property, `Node` its runtime name, and in dev the span's tooltip is the same `[why-run]` chain the console prints. @@ -70,8 +71,9 @@ Give scopes names before you record here for the same reason as above: an unname The **Server** track reads the response's `Server-Timing` header, which a dev server writes for every request and an observe server writes while something on the server subscribes to the matching record: `solid-invocation` from an `"invocation"` record on a server-function response, and on a document `solid-shell` from the `"render"` record plus one `solid-boundary` per `Loading` boundary the shell waited on. The document's own metrics appear as `shell · server` and `boundary › · server` when the tracks enable, and a call's appear beneath its span once the browser's resource timing places it. -`enablePerformanceTracks()` takes its own hold on the engine with `log: false`, so it coexists with the console session above or with an adapter, and the console stays quiet only while nothing else asks for the log. -It accepts `attribution` options for that hold, `minMs` to floor the spans it paints (`0` in dev, `0.05` in the observe build), and `rich` to switch between `performance.measure` entries with tooltips and properties and plain `console.timeStamp` entries. +`enablePerformanceTracks()` takes its own records-only hold on the engine, with `log: false` and `checks: false`, so it coexists with the console session above or with an adapter, and the console stays quiet only while nothing else asks for the log. +Pass `enablePerformanceTracks({ attribution: { checks: true } })` to run the cost checks (`hotRuns`, `hotTime`, `wideDeps`, `unstableMemos`, `fanOut`, `wastedRecompute`) under the tracks' own hold. +It accepts other `attribution` options for that hold, `minMs` to floor the spans it paints (`0` in dev, `0.05` in the observe build), and `rich` to switch between `performance.measure` entries with tooltips and properties and plain `console.timeStamp` entries. An observe build's tracks inherit the engine's `values: "none"`, so no value previews and no element text reach the panel; pass `attribution: { values: "labels" }` for the control labels, or in dev `"none"` for a timeline without user data. The [`enablePerformanceTracks` reference](/reference/solid-web/performance-tracks/enable-performance-tracks) lists the options. @@ -194,9 +196,11 @@ const Account = lazy(() => import("./account/Account")); Render `` for the first time and the dynamic import starts; while it is in flight the component suspends through the nearest `Loading` boundary, which shows the skeleton. Call `Account.preload()` to start the import early, for example from a link's `onMouseEnter`. +For a module that is not a component, such as a route table or a formatting library, [`lazyModule`](/reference/solid-js/components-context/lazy) is the same cached import with `.preload()`, and the server can preload it before hydration. With Solid Router the split usually happens at the route. -A route whose `component` is a `lazy` component has its chunk loaded when the user hovers, focuses, or touches a link to it, and a `children: () => import("./account/routes")` thunk defers a whole route table the same way. +A route whose `component` is a `lazy` component has its chunk loaded on first navigation, and a `children: () => import("./account/routes")` thunk defers a whole route table the same way. +Link preloading is opt-in: create the router with `createRouter({ routes, preloadLinks: intentPreload() })` and the chunk loads when the user hovers, focuses, or touches a link to it. [Load a route subtree lazily](/routing/solid-router/route-definitions#load-a-route-subtree-lazily) and [Preloading from links](/routing/solid-router/data#preloading-from-links) cover both. :::caution[A chunk is a round trip] @@ -301,7 +305,7 @@ Measure timings on a production build; use the development build to find out whi - Put a shared or expensive derivation in a memo, and put an equality boundary on a primitive so a change that settles to the same value stops there. - Hand `For` the store's objects; a `map` that produces new objects rebuilds every row. - Derive independent requests from inputs you already have, so they start together. -- Split code with `lazy` at the boundary between the first page and the rest, and let the router preload chunks on hover. +- Split code with `lazy` at the boundary between the first page and the rest, and turn on `preloadLinks: intentPreload()` so the router preloads chunks on hover. - Wrap each streamed region in a `Loading` boundary so the shell does not wait for its data. - Keep per-row flags in a store or projection keyed by id, so a change touches the rows whose key flipped. diff --git a/src/routes/(5)guides/(12)typescript.mdx b/src/routes/(5)guides/(12)typescript.mdx index 2ab413738..c57f4c900 100644 --- a/src/routes/(5)guides/(12)typescript.mdx +++ b/src/routes/(5)guides/(12)typescript.mdx @@ -229,7 +229,9 @@ The draft is a `Cart`, so a write to the wrong property or a half-built item is Because `Store` is `Cart`, the proxy passes anywhere a `Cart` or a `CartItem[]` is expected, such as ``, with no cast. `reconcile(value)` returns `(state: Cart) => Cart`, which is exactly what the setter takes. -The function form, `createStore(async () => getCart(), seed)`, is typed by its seed and return value, and [`createProjection`](/reference/solid-js/stores/create-projection) returns a `Store` of its seed type. +The function form without a seed, `createStore(async () => getCart())`, is typed by the function's return value, so `cart` is a `Store`; this form takes object roots only. +With a seed, `createStore((draft) => { ... }, seed)`, the store is typed by the seed and the function, and the seed must be a complete `T`: a partial object such as `{ items: [] }` for a `Cart` is a type error, and an array store takes `[]`. +[`createProjection`](/reference/solid-js/stores/create-projection) follows the same two forms and returns a `Store`. [Stores](/concepts/stores#update-with-a-draft) explains what the draft does at runtime. ## Events and refs diff --git a/src/routes/(5)guides/(14)testing.mdx b/src/routes/(5)guides/(14)testing.mdx index 23fb21060..0ebd31efb 100644 --- a/src/routes/(5)guides/(14)testing.mdx +++ b/src/routes/(5)guides/(14)testing.mdx @@ -192,6 +192,25 @@ pnpm test --run The templates demonstrate component tests, not browser-wide end-to-end flows. See the [Vitest browser-mode guide](https://vitest.dev/guide/browser/) for browser locators, interactions, and browser-mode limitations when a component test needs those APIs. +An end-to-end test against a server-rendered page has to wait until the page is interactive before it clicks. +The experimental [`onHydrationEnd`](/reference/solid-js/advanced/manual-hydration/on-hydration-end) runs its callback once the hydration phase ends, so the client entry can set a marker for the test to wait for: + +```tsx +// src/entry-client.tsx +import { onHydrationEnd } from "solid-js"; +import { hydrate } from "@solidjs/web"; +import App from "./App"; + +hydrate(() => , document); +onHydrationEnd(() => { + document.documentElement.dataset.hydrated = ""; +}); +``` + +The test then waits for `html[data-hydrated]` before it interacts with the page. +Register the callback after `hydrate()`; registered before hydration starts, it runs in the next microtask without waiting. +The marker means the hydration phase ended, not that it succeeded, so use it for tests and not to change what the app renders. + ## Test server code in a Node project A fullstack app needs separate client and server test environments. diff --git a/src/routes/(5)guides/(15)observability.mdx b/src/routes/(5)guides/(15)observability.mdx index b0fff0e63..e2faa6b9c 100644 --- a/src/routes/(5)guides/(15)observability.mdx +++ b/src/routes/(5)guides/(15)observability.mdx @@ -98,14 +98,18 @@ configureServerErrors({ `kind` and `handling` say which road the failure took: -| `kind` | `handling` | What happened | -| ----------------- | ----------- | ----------------------------------------------------------------------------------- | -| `render` | `fallback` | An `Errored` boundary rendered its fallback. | -| `render` | `client` | A `Loading` fragment rejected; the client re-renders that subtree. | -| `render` | `failed` | Nothing contained it; the request fails. | -| `render` | `serialize` | A value written to the hydration stream would not serialize. | -| `server-function` | `thrown` | The function threw. `direct` is `true` for an in-process call during a render. | -| `server-function` | `channel` | A rejection escaped through a returned stream or iterable after the head committed. | +| `kind` | `handling` | What happened | +| ----------------- | ----------- | ----------------------------------------------------------------------------------------- | +| `render` | `fallback` | An `Errored` boundary rendered its fallback. | +| `render` | `client` | A `Loading` fragment rejected; the client re-renders that subtree. | +| `render` | `failed` | Nothing contained it; the request fails. | +| `render` | `serialize` | A value written to the hydration stream would not serialize. | +| `server-function` | `thrown` | The function threw. `direct` is `true` for an in-process call during a render. | +| `server-function` | `channel` | A rejection escaped through a returned stream or iterable after the head committed. | +| `request` | `failed` | Code outside the runtime, such as middleware, passed a failure to `reportRequestFailure`. | + +A request failure the runtime never met, such as a throw a middleware catches before any render starts, reaches the hook only when that code calls `reportRequestFailure(error, event)` from `@solidjs/web`; [Catching errors](/building-apps/middleware-and-api-routes#catching-errors) shows the call. +The context then carries the request's `event`, and the hook's return value is ignored, as it is for every `failed` handling. The hook runs inside the request scope, so `getRequestEvent()` works in it. `renderToStream(App, { onError })` and `renderToString(App, { onError })` take a per-request hook that wins over the ambient one for that request. diff --git a/src/routes/(5)guides/(7)protected-routes.mdx b/src/routes/(5)guides/(7)protected-routes.mdx index 8ec6fbdb6..353b00898 100644 --- a/src/routes/(5)guides/(7)protected-routes.mdx +++ b/src/routes/(5)guides/(7)protected-routes.mdx @@ -133,11 +133,11 @@ Open `/account/orders` signed out from a bookmark and the browser lands on `/sig Click **Account** in the header signed out and the URL becomes the same sign-in address without the account page painting first: the memo in `RequireUser` is pending, so the navigation is held, and the redirect replaces its target before anything commits. Sign in, and the next visit to the account area runs the check again and finds the user, because a router action's completion invalidates every cached query. -The preload is what starts the check as soon as the route matches, and on hover, before `RequireUser` exists; the memo is what holds the navigation until the check answers. +The preload is what starts the check as soon as the route matches, and when a [link preload strategy](/routing/solid-router/data#preloading-from-links) warms the link, before `RequireUser` exists; the memo is what holds the navigation until the check answers. Both call the same function. Its body runs on the server either way: in-process during server rendering, where it shares the request's `getCurrentUser` result with the header, and through the server-function endpoint during a navigation, where the thrown redirect travels back to the router as the response. -Hovering the **Account** link runs the preload with the `"preload"` intent, which starts the request and does not act on its outcome, so a hover never redirects; the click that follows does, whether it reuses that answer or fetches a new one. +With `preloadLinks: intentPreload()` on the router, hovering the **Account** link runs the preload with the `"preload"` intent, which starts the request and does not act on its outcome, so a hover never redirects; the click that follows does, whether it reuses that answer or fetches a new one. :::deep-dive[What the router does with a thrown redirect] `query` treats a value thrown by the wrapped function the same way as a returned one. diff --git a/src/routes/(6)migration/(0)from-solid-1.mdx b/src/routes/(6)migration/(0)from-solid-1.mdx index b12803916..246c07306 100644 --- a/src/routes/(6)migration/(0)from-solid-1.mdx +++ b/src/routes/(6)migration/(0)from-solid-1.mdx @@ -193,14 +193,25 @@ Delete `produce(...)` wrappers and pass the producer body directly to the setter Replace `createMutable` and `modifyMutable` with `createStore` and explicit draft setters. Store writes follow the same staged-write timing as signals. -[`storePath`](/reference/solid-js/advanced/store-advanced/store-path) is a migration helper for path setters that cannot be converted at once: +The setter takes no path arguments. +A Solid 1 path setter becomes a draft write to the same path; ranges and filters become ordinary loops, and setting `undefined` to remove a key becomes `delete`: ```ts -setState(storePath("user", "address", "city", "Paris")); -``` +// Solid 1: setState("items", { from: 1, to: 4, by: 2 }, 99) +setState((draft) => { + for (let i = 1; i <= 4; i += 2) draft.items[i] = 99; +}); -Prefer draft setters for new code. -`storePath` also supports indexed, filtered, and ranged paths, so preserve the original path semantics when converting complex setters. +// Solid 1: setState("todos", (todo) => todo.done, "archived", true) +setState((draft) => { + for (const todo of draft.todos) if (todo.done) todo.archived = true; +}); + +// Solid 1: setState("nickname", undefined) +setState((draft) => { + delete draft.nickname; +}); +``` `reconcile` now runs against the selected draft: @@ -318,6 +329,7 @@ Apply these direct UI migrations: `Dynamic` is deprecated. Create the component once, outside the JSX, and render it wherever it is needed; a falsy source renders nothing, as before. - Replace direct `createDynamic(source, props)` calls with `dynamic(source)`. +- Use [`dynamicComponent`](/reference/solid-web/components/dynamic) from `solid-js` instead when the source only ever answers with a component, never a tag name; it does not keep the element runtime in the bundle. Update removed JSX forms: @@ -325,6 +337,8 @@ Update removed JSX forms: - Replace `use:directive` with a `ref` callback or directive factory, and use a `ref` array to compose callbacks. - Replace `on:` and `oncapture:` with camel-case Solid event props. Use a `ref` callback and `addEventListener` when native listener options are required. +- Rename lowercase handlers such as `onclick={fn}` to `onClick`. + Only `on` followed by an uppercase letter is an event handler; a lowercase `onclick` or a leftover `on:click` compiles to a plain attribute without an error, and development warns `[LOWERCASE_EVENT_ATTRIBUTE]` when it receives a function. - Replace `attr:` and `bool:` with standard attributes and boolean presence. - Remove `/*@once*/`. Keep values reactive, use DOM default properties such as `defaultValue` for initial state, or take a narrow JavaScript snapshot with `untrack`. diff --git a/src/routes/(6)migration/(3)from-solid-router.mdx b/src/routes/(6)migration/(3)from-solid-router.mdx index 402c66c0f..144abfe24 100644 --- a/src/routes/(6)migration/(3)from-solid-router.mdx +++ b/src/routes/(6)migration/(3)from-solid-router.mdx @@ -72,6 +72,16 @@ export const Router = createRouter({ export const { paths } = Router; ``` +Link preloading on hover, focus, and touch is off by default in Router 2. +To keep the 1.x behavior, add `preloadLinks: intentPreload()` to the config. +Drop a `preload={false}` prop from the old ``; leaving `preloadLinks` out has the same effect. + +```tsx +import { createRouter, intentPreload } from "@solidjs/router"; + +export const Router = createRouter({ routes, preloadLinks: intentPreload() }); +``` + Mount the returned instance as the provider component. Replace the old `root` prop with the provider's function child. The factory-level `preload` result is available as `props.data`. @@ -231,7 +241,14 @@ import { Router } from "./router"; ; ``` -The router adds state attributes to anchors that it claims: +The router adds state attributes to anchors that it claims. +`data-pending` needs the `pendingLinks` plugin: + +```tsx +import { createRouter, pendingLinks } from "@solidjs/router"; + +export const Router = createRouter({ routes, links: pendingLinks }); +``` ```css nav a[data-active] { @@ -247,13 +264,14 @@ a[data-pending] { } ``` -Use `[data-active]` for an exact or descendant match. -Use `[aria-current="page"]` for an exact match. +Use `[data-active]` for an exact or descendant path match. +Use `[aria-current="page"]` for an exact match of the path and query; parameter order and the hash are ignored. +The router leaves an `aria-current` value the author set on the anchor alone. The root path has exact-only active behavior. Move old link props onto the anchor: -- `noScroll`, `replace`, `state`, and `preload` become anchor attributes, typed on `` by `@solidjs/web`; like every other HTML attribute they are lowercase, so `noScroll` becomes `noscroll`. +- `noScroll`, `replace`, `state`, and `preload` become anchor attributes, typed on `` by the router's augmentation of `JSX.AnchorHTMLAttributes`; like every other HTML attribute they are lowercase, so `noScroll` becomes `noscroll`. - `activeClass` and `inactiveClass` become CSS selectors for the state attributes. - `end` becomes an exact-match selector with `[aria-current="page"]`. @@ -284,8 +302,12 @@ See [Navigation and typed paths](/routing/solid-router/navigation) for the remai ## Type params and search values -`useParams()` and an untyped `useSearchParams()` keep their runtime behavior. -Path parameters remain strings, and search values remain strings or string arrays without a schema. +An untyped `useSearchParams()` keeps its runtime behavior: search values remain strings or string arrays without a schema. +Path parameters remain strings, and are now decoded. +`useParams()`, `props.params`, and `Router.match()` return `"猫"` for `/item/%E7%8C%AB`, and match filters test the decoded value. +Remove any `decodeURIComponent` call on a parameter, or the value is decoded twice. +Encode a parameter when interpolating it into a URL by hand, or build the URL with `paths`. +`location.pathname` keeps the URL's encoding. Router 2 adds definition-driven types. Use `defineRoute` when the component or preload is declared with the route: @@ -713,7 +735,9 @@ See the [router factory reference](/reference/solid-router/router-factory) and [ - [ ] Move `root` to the provider's function child and `rootPreload` to factory `preload`. - [ ] Convert nested routes to `children` arrays and keep layouts rendering `props.children`. - [ ] Replace `Router`, `HashRouter`, and `MemoryRouter` components with `createRouter` and a history adapter. -- [ ] Replace `` with `` and move active styling to state-attribute selectors. +- [ ] Add `preloadLinks: intentPreload()` if links should preload on hover, focus, and touch. +- [ ] Replace `` with `` and move active styling to state-attribute selectors; add `links: pendingLinks` for `data-pending`. +- [ ] Remove `decodeURIComponent` calls on route parameters. - [ ] Replace `` and rename `useCurrentMatches`. - [ ] Replace `cache` with `query`. - [ ] Remove `createAsync`, `createAsyncStore`, and `useSubmission`. diff --git a/src/routes/(7)glossary.mdx b/src/routes/(7)glossary.mdx index 81d7c343c..09665c1c5 100644 --- a/src/routes/(7)glossary.mdx +++ b/src/routes/(7)glossary.mdx @@ -418,6 +418,8 @@ See [Handle thrown errors](/building-apps/server-functions/mutations-and-respons ### Seed The starting object a projection or an async store reconciles its results into, which gives the root proxy a stable identity across recomputations; with an async function it is not shown as a first answer unless `seedLoadingValue` says so. +It is optional for an object root whose function takes no draft and returns the whole value, `createStore(async () => api.cart())`, and required for an array root, `[]`. +When given, it is a complete value of the store's type, not a partial one. See [Derive a store with a projection](/concepts/stores#derive-a-store-with-a-projection). diff --git a/src/routes/reference/(3)solid-router/(0)index.mdx b/src/routes/reference/(3)solid-router/(0)index.mdx index 5154d7329..714056fa2 100644 --- a/src/routes/reference/(3)solid-router/(0)index.mdx +++ b/src/routes/reference/(3)solid-router/(0)index.mdx @@ -19,10 +19,10 @@ Solid Router is published through three package entries. ## Reference groups -- [Router factory](/reference/solid-router/router-factory): `createRouter`, `defineRoute`, and `defineRoutes` +- [Router factory](/reference/solid-router/router-factory): `createRouter`, `defineRoute`, `defineRoutes`, link preload strategies, and `pendingLinks` - [Routes and typed paths](/reference/solid-router/routes-and-paths): route definitions, path patterns, `int`, instance paths, and matching - [Navigation primitives](/reference/solid-router/navigation): location, navigation, matching, search, preloading, link state, and leave guards -- [Data APIs](/reference/solid-router/data): `query`, `revalidate`, `action`, `useAction`, and `useSubmissions` +- [Data APIs](/reference/solid-router/data): `query`, `liveQuery`, `revalidate`, `action`, `useAction`, and `useSubmissions` - [History adapters](/reference/solid-router/history): browser, hash, and memory histories - [File-system adapter](/reference/solid-router/filesystem): `fileRoutes` and `defineFileRoute` - [Server integration](/reference/solid-router/server): `createFlightDataCollector` diff --git a/src/routes/reference/(3)solid-router/(1)router-factory.mdx b/src/routes/reference/(3)solid-router/(1)router-factory.mdx index e0984834a..c738c38ea 100644 --- a/src/routes/reference/(3)solid-router/(1)router-factory.mdx +++ b/src/routes/reference/(3)solid-router/(1)router-factory.mdx @@ -10,7 +10,16 @@ source_path: "src/routers/factory.tsx" ## Import ```ts -import { createRouter, defineRoute, defineRoutes } from "@solidjs/router"; +import { + createRouter, + defineRoute, + defineRoutes, + eagerPreload, + intentPreload, + pendingLinks, + tapPreload, + viewportPreload, +} from "@solidjs/router"; ``` ## `createRouter` @@ -36,7 +45,8 @@ interface RouterConfig< singleFlight?: boolean; actionBase?: string; explicitLinks?: boolean; - preloadLinks?: boolean; + links?: LinksPlugin; + preloadLinks?: LinkPreload | readonly LinkPreload[]; scrollRestoration?: boolean; transformUrl?: (url: string) => string; } @@ -49,7 +59,8 @@ interface RouterConfig< - `singleFlight` defaults to `true`. - `actionBase` defaults to `/_server`. - `explicitLinks` defaults to `false`. -- `preloadLinks` defaults to `true`. +- `links` adds state to claimed anchors. Pass [`pendingLinks`](#pendinglinks) for `data-pending`. Client-only. +- `preloadLinks` takes one [link preload strategy](#link-preload-strategies) or an array of them. Links do not preload by default. Client-only. - `scrollRestoration` defaults to `true` with the default browser history. - `transformUrl` rewrites pathnames before matching and preloading. @@ -92,6 +103,112 @@ interface RouterProps { A request event takes precedence. The client ignores `url`. +## Link preload strategies + +A `LinkPreload` is a strategy for the `preloadLinks` option. +It runs once under the router's owner on the client and cleans up with it. +Each strategy warms the link's route code, and runs the matched routes' `preload` functions when `data` is `true`. +A link with `preload="false"` is never preloaded. + +```ts +type LinkPreload = (ctx: LinkPreloadContext) => void; +``` + +```ts +const Router = createRouter({ + routes, + preloadLinks: [intentPreload(), viewportPreload()], +}); +``` + +### `intentPreload` + +```ts +function intentPreload(options?: { + delay?: number; + data?: boolean; +}): LinkPreload; +``` + +Preloads a link when the pointer rests on it for `delay` ms (default `20`), when it takes focus, and on `touchstart`. +A link preloads once per visit, whichever of these comes first. +`data` defaults to `true`. + +### `tapPreload` + +```ts +function tapPreload(options?: { data?: boolean }): LinkPreload; +``` + +Preloads a link on `pointerdown`, ahead of its click. +`data` defaults to `true`. + +### `viewportPreload` + +```ts +function viewportPreload(options?: { + all?: boolean; + data?: boolean; + delay?: number; + rootMargin?: string; +}): LinkPreload; +``` + +Preloads a link once it has stayed in the viewport for `delay` ms (default `100`), when the browser is next idle. +Each link preloads once, and again after its `href` changes. +Applies to links with `preload="viewport"`, or with `all: true` to every link except `preload="false"`. +`rootMargin` is passed to the shared `IntersectionObserver`. +`data` defaults to `false`. +Skips preloading when the browser reports Save-Data or a 2g connection. + +### `eagerPreload` + +```ts +function eagerPreload(options?: { all?: boolean; data?: boolean }): LinkPreload; +``` + +Preloads links once the page has loaded and the browser is idle, including links mounted later. +Applies to links with `preload="eager"`, or with `all: true` to every link except `preload="false"`. +`data` defaults to `false`. +Skips preloading when the browser reports Save-Data or a 2g connection. + +## `pendingLinks` + +A `LinksPlugin` that adds `data-pending` to claimed anchors whose path covers the in-flight destination of a link click or `navigate()` call. +Back and forward navigation does not set it. +The attribute is removed when the navigation lands. + +```ts +const pendingLinks: LinksPlugin; +``` + +```ts +const Router = createRouter({ routes, links: pendingLinks }); +``` + +```css +a[data-pending] { + opacity: 0.6; +} +``` + +`useLinkState().pending` works without this plugin. + +### `LinksPlugin` + +```ts +type LinksPlugin = ( + router: RouterContext, + base: string +) => { + track(): unknown; + pending(target: string | undefined): boolean; +}; +``` + +The router builds the plugin once with its base path. +The claims sweep re-runs when `track()` changes, and reads `pending(target)` for each anchor while `track()` is truthy. + ## `defineRoutes` Preserves literal types for an extracted route tuple and returns the same value. diff --git a/src/routes/reference/(3)solid-router/(2)routes-and-paths.mdx b/src/routes/reference/(3)solid-router/(2)routes-and-paths.mdx index b72bae0f1..6879becde 100644 --- a/src/routes/reference/(3)solid-router/(2)routes-and-paths.mdx +++ b/src/routes/reference/(3)solid-router/(2)routes-and-paths.mdx @@ -100,6 +100,23 @@ const route = defineRoute({ Runtime route parameters remain strings. +### Parameter decoding + +The router decodes each dynamic segment with `decodeURIComponent` after matching. +`useParams()`, `props.params`, `useMatch`, and `Router.match` return the decoded values, and match filters test them. + +| Pattern | URL | Parameter | +| -------------- | ----------------- | --------------- | +| `/item/:id` | `/item/%E7%8C%AB` | `id: "猫"` | +| `/:name/view` | `/a%2Fb/view` | `name: "a/b"` | +| `/files/*rest` | `/files/a%20b/c` | `rest: "a b/c"` | +| `/item/:id` | `/item/100%` | `id: "100%"` | + +An encoded slash stays inside its parameter. +A splat parameter is decoded segment by segment. +A malformed escape is left as written. +Matched paths and `location.pathname` keep the URL's encoding. + ## `Router.paths` `Router.paths` has type `RoutePaths`. @@ -156,6 +173,7 @@ interface OutputMatch { Returns matches from root to leaf. Returns `[]` when no route matches. +`params` holds decoded values; `match` keeps the URL's encoding. The router's `transformUrl`, `base`, and resolved lazy subtrees apply. ## Route component types diff --git a/src/routes/reference/(3)solid-router/(3)navigation.mdx b/src/routes/reference/(3)solid-router/(3)navigation.mdx index bbbf97879..3e4016eaa 100644 --- a/src/routes/reference/(3)solid-router/(3)navigation.mdx +++ b/src/routes/reference/(3)solid-router/(3)navigation.mdx @@ -13,6 +13,7 @@ source_path: "src/routing.ts" import { useBeforeLeave, useHref, + useInRouter, useIsRouting, useLinkState, useLocation, @@ -26,7 +27,20 @@ import { } from "@solidjs/router"; ``` -All primitives on this page require a mounted router context. +All primitives on this page except `useInRouter` throw when called outside a router. +All of them throw when called with no reactive owner, such as from an event handler. + +## `useInRouter` + +```ts +function useInRouter(): boolean; +``` + +Returns whether the calling component is rendered under a router. + +```ts +const location = useInRouter() ? useLocation() : undefined; +``` ## `useNavigate` @@ -87,6 +101,11 @@ function useParams

( Returns the reactive merged parameters for the current route chain. The path argument is a type witness and is not matched at runtime. +Each value is decoded with `decodeURIComponent` after matching, so `/item/%E7%8C%AB` gives `id: "猫"`. +An encoded slash stays inside its parameter: `/a%2Fb/view` matched by `/:name/view` gives `name: "a/b"`. +A splat parameter is decoded segment by segment. +A malformed escape is left as written. + ## `useSearchParams` ```ts @@ -166,8 +185,8 @@ interface LinkState { } ``` -`active` includes descendants unless `end` is true. -`current` is exact. +`active` compares the path only and includes descendants unless `end` is true. +`current` requires the same path and the same query, ignoring parameter order and the hash. `pending` matches the active in-flight target. ## `useResolvedPath` diff --git a/src/routes/reference/(3)solid-router/(4)data.mdx b/src/routes/reference/(3)solid-router/(4)data.mdx index 3c31e8d39..3398d6f59 100644 --- a/src/routes/reference/(3)solid-router/(4)data.mdx +++ b/src/routes/reference/(3)solid-router/(4)data.mdx @@ -1,7 +1,7 @@ --- title: "Data APIs" version: "2.0" -description: "Reference for Solid Router query caching, revalidation, actions, action invocation, and settled submissions." +description: "Reference for Solid Router query caching, live queries, revalidation, actions, action invocation, and settled submissions." source_repo: "solidjs/solid-router" source_ref: "next" source_path: "src/data/index.ts" @@ -12,6 +12,7 @@ source_path: "src/data/index.ts" ```ts import { action, + liveQuery, query, revalidate, useAction, @@ -64,6 +65,50 @@ query.clear(): void; `query.get` throws when no entry exists. `query.set` does not accept a promise value. +## `liveQuery` + +```ts +function liveQuery any>( + fn: T, + name: string +): LiveFunction; +``` + +Declares a keyed query over an async-iterable producer. +Each value the producer yields is the next value of the query. +The producer must yield the current state each time it is invoked. + +```ts +type LiveFunction any> = (( + ...args: Parameters +) => AsyncIterable) & { + key: string; + keyFor(...args: Parameters): string; + status(...args: Parameters): LiveQueryStatus; +}; + +type LiveQueryStatus = + "idle" | "connecting" | "connected" | "reconnecting" | "closed"; +``` + +`Value` is the producer's yielded type, or its resolved return type when it does not return an iterable. + +```ts +const roomMessages = liveQuery(getMessages, "messages"); + +const messages = createMemo(() => roomMessages(props.room)); +``` + +- Keys combine `name` with the arguments, as `query` keys do. +- Consumers of one key share one connection. A late consumer receives the latest value first. The connection closes when the last consumer leaves. +- A stream that has delivered a value and then fails with a network error or a 5xx status reconnects with exponential backoff. The latest value stays available meanwhile. +- A failure before the first value, or a 4xx status, ends the channel and throws to consumers. +- When `fn` is a server function without a declared method, `liveQuery` wraps it as a GET server function. +- A call under preload intent connects the channel and holds it open for 5 seconds. +- Mutations do not revalidate live keys. `revalidate(key)` with `force` reconnects the matching channels. +- On the server, the document renders the first value. Hydration adopts it and reconnects. +- `status(...args)` is a reactive read of the channel's connection state. + ## `revalidate` ```ts @@ -117,7 +162,9 @@ Server functions supply their own URL. ``` Only POST forms are accepted by delegated action handling. -The form has `aria-busy="true"` while its delegated submission is running. +The form has `aria-busy="true"` from its delegated submission until the action's transition commits. +Busy state is keyed by the form's `action` URL, so every form posting to that URL shows it. +The router does not overwrite or remove an `aria-busy` value the author set. ### `with` @@ -133,6 +180,8 @@ Registration under an owner is removed when the owner is disposed. Registers a callback for every completed invocation. The callback receives a `Submission`, including for void, redirect, and metadata-only outcomes. +Callbacks run when the action's transition commits, after any revalidation or redirect it holds has rendered. +The submission record appears in `useSubmissions` at the same point. ## `useAction` @@ -143,6 +192,7 @@ function useAction( ``` Binds direct action invocation to the current router context. +The returned promise resolves when the action body finishes, which can be before the action settles and `onSettled` runs. ## `useSubmissions` diff --git a/src/routes/reference/(3)solid-router/(8)types.mdx b/src/routes/reference/(3)solid-router/(8)types.mdx index 411a2ea9d..3c922239d 100644 --- a/src/routes/reference/(3)solid-router/(8)types.mdx +++ b/src/routes/reference/(3)solid-router/(8)types.mdx @@ -25,6 +25,8 @@ import type { - `RouterInstance` is the provider component with `paths`, `routes`, `config`, and `match`. - `RouterProps` contains the server-only `url` and root render-prop `children`. - `DefinedRoute` is the preserved return type of `defineRoute`. +- `LinkPreload` is a link preload strategy for `preloadLinks`. +- `LinksPlugin` is a link claims plugin for `links`, such as `pendingLinks`. See [Router factory](/reference/solid-router/router-factory) for signatures. @@ -129,6 +131,8 @@ See [History adapters](/reference/solid-router/history) for the supported built- ## Data - `CachedFunction` is returned by `query`. +- `LiveFunction` is returned by `liveQuery`. +- `LiveQueryStatus` is the connection status read by `LiveFunction.status`. - `Action` is returned by `action`. - `Submission` is a settled action record. From 5e8923074c22a7c60b2d81f0615a3cd14b5462e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 01:16:26 +0000 Subject: [PATCH 3/3] docs(router): say held update, not transition, per the Solid Terms table Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y4HXPRqXPnqriNxDyin6eK --- src/routes/reference/(3)solid-router/(4)data.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/reference/(3)solid-router/(4)data.mdx b/src/routes/reference/(3)solid-router/(4)data.mdx index 3398d6f59..2b7bada76 100644 --- a/src/routes/reference/(3)solid-router/(4)data.mdx +++ b/src/routes/reference/(3)solid-router/(4)data.mdx @@ -162,7 +162,7 @@ Server functions supply their own URL. ``` Only POST forms are accepted by delegated action handling. -The form has `aria-busy="true"` from its delegated submission until the action's transition commits. +The form has `aria-busy="true"` from its delegated submission until the action's held update commits. Busy state is keyed by the form's `action` URL, so every form posting to that URL shows it. The router does not overwrite or remove an `aria-busy` value the author set. @@ -180,7 +180,7 @@ Registration under an owner is removed when the owner is disposed. Registers a callback for every completed invocation. The callback receives a `Submission`, including for void, redirect, and metadata-only outcomes. -Callbacks run when the action's transition commits, after any revalidation or redirect it holds has rendered. +Callbacks run when the action's held update commits, after any revalidation or redirect it holds has rendered. The submission record appears in `useSubmissions` at the same point. ## `useAction`