diff --git a/scripts/extract-solid-ref.mjs b/scripts/extract-solid-ref.mjs index cfa223888..726147b3e 100644 --- a/scripts/extract-solid-ref.mjs +++ b/scripts/extract-solid-ref.mjs @@ -102,6 +102,7 @@ const ADVANCED_ROUTE_ORDER = { const CANONICAL_ROUTES = { createEffect: ["reactivity/create-effect.mdx", "Reactivity"], + createDeferred: ["reactivity/create-deferred.mdx", "Reactivity"], createMemo: ["reactivity/create-memo.mdx", "Reactivity"], createOptimistic: ["reactivity/create-optimistic.mdx", "Reactivity"], createSignal: ["reactivity/create-signal.mdx", "Reactivity"], @@ -613,6 +614,7 @@ const FOLD_INTO = { ServerFunctionsClientConfig: "configureServerFunctionsClient", PrepareRequestContext: "configureServerFunctionsClient", PrepareRequestHook: "configureServerFunctionsClient", + ServerFunctionsSend: "configureServerFunctionsClient", ServerFunctionsServerConfig: "configureServerFunctionsServer", ServerFunctionEvent: "configureServerFunctionsServer", HandleServerFunctionOptions: "handleServerFunctionRequest", diff --git a/src/routes/(2)concepts/(5)mutations.mdx b/src/routes/(2)concepts/(5)mutations.mdx index e2cc4fd50..8269b9fdd 100644 --- a/src/routes/(2)concepts/(5)mutations.mdx +++ b/src/routes/(2)concepts/(5)mutations.mdx @@ -363,5 +363,5 @@ Remove the call and assert after the action's promise resolves. - [Forms](/guides/forms): the same actions behind a `
`, with progressive enhancement, validation, and errors returned from the server. - [Mutations and responses](/building-apps/server-functions/mutations-and-responses): writing `api.add` as a `"use server"` function and returning redirects, statuses, and safe errors. - [Boundaries](/concepts/boundaries): where `Errored` catches the errors an action does not, and how an errored region recovers. -- [Data loading and mutations](/routing/solid-router/data): Solid Router's `action` and `query` add submissions, caching, and revalidation on top of these primitives. +- [Data loading and mutations](/routing/solid-router/data): with Solid Router mounted, the same actions call server functions and the router applies their revalidation and redirects; `query` adds caching and `formAction` adds forms. - [State management](/guides/state-management): where the cart, the user, and the filter live, and why `createCart()` runs inside a provider. 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 db76ff525..fd67ae414 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 @@ -223,10 +223,10 @@ Render `` and the form posts to the The router marks the form `aria-busy` while it runs and, once it settles, records a submission whose `error` is the thrown envelope's value, which the [Forms guide](/guides/forms) reads to show messages next to the fields. Keep the server function a named export and wrap it separately, as above, so a test or an API route can call `addToCart` without the form. -When the function exists only for the form, write the body inline, `formAction(async (form: FormData) => { "use server"; ... })`; the [Data loading and mutations](/routing/solid-router/data#mutate-with-actions) page uses that shape throughout, and the two behave the same. +When the function exists only for the form, write the body inline, `formAction(async (form: FormData) => { "use server"; ... })`; the two behave the same, and [Data loading and mutations](/routing/solid-router/data#add-a-form-with-formaction) shows both. :::note[Which action] -`formAction` is from `@solidjs/router`; earlier releases called it `action`, and that name remains as a deprecated alias. +`formAction` is from `@solidjs/router`. `solid-js` exports its own [`action`](/reference/solid-js/lifecycle-actions/action), which runs a generator as a reactive transaction and has no URL; [Mutations](/concepts/mutations) covers it. A form needs the router's `formAction`. ::: diff --git a/src/routes/(3)building-apps/(3)server-functions/(5)progressive-enhancement.mdx b/src/routes/(3)building-apps/(3)server-functions/(5)progressive-enhancement.mdx index 220ff4730..a76598c6f 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(5)progressive-enhancement.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(5)progressive-enhancement.mdx @@ -13,9 +13,9 @@ The server-function runtime recognizes a browser form post at that URL, decodes Application code reaches this through a router. Solid Router's `formAction()` wraps the server function in a value that serializes to that URL, so the same `` works before hydration and is taken over by the router after it. -Most applications need only the [router action](#post-a-form-through-a-router-action) section; the rest of the page is the transport underneath, for forms outside the router and for integrations that build their own form helpers. +Most applications need only the [form action](#post-a-form-through-a-form-action) section; the rest of the page is the transport underneath, for forms outside the router and for integrations that build their own form helpers. -## Post a form through a router action +## Post a form through a form action Declare a server function that takes one `FormData` argument, and wrap it with `formAction` from `@solidjs/router`: diff --git a/src/routes/(3)building-apps/(3)server-functions/(7)durable-generation.mdx b/src/routes/(3)building-apps/(3)server-functions/(7)durable-generation.mdx index bf3efeea1..d6f772af6 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(7)durable-generation.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(7)durable-generation.mdx @@ -10,17 +10,17 @@ It is the wrong one for a summary of a forty-message order dispute: the shopper The difference is who owns the generation. In the streaming guide the request owns it, and the reactive graph cancels it. -On this page a job on the server owns it: an action starts the job and returns, the job writes the reply into storage as the model produces it, and every page that shows the reply reads what is stored through a live source. +On this page a job on the server owns it: a server function starts the job and returns, the job writes the reply into storage as the model produces it, and every page that shows the reply reads what is stored through a live source. Pick the request-scoped shape when the reader is the only consumer and an abandoned answer has no value; pick this shape when the reply must finish without its reader, be found again on reload, or be read from more than one tab. Two deliveries are shown. The client-owned one sends the reply as data through [`live()`](/reference/solid-web/server-functions/live) and renders it in the browser. The server-owned one renders the whole thread on the server as a live server component, which is the shape of the `chat` example in the Solid repository; server components are an experimental preview, and that section says so. -The job, the storage, and the actions are the same in both. +The job, the storage, and the server functions are the same in both. ## The job -Three modules carry the shape: a store the job writes into, a registry of running jobs that no request owns, and the actions that start and stop one. +Three modules carry the shape: a store the job writes into, a registry of running jobs that no request owns, and the server functions that start and stop one. The store keeps each reply and tells watchers when it changes: @@ -115,7 +115,7 @@ The signal it receives is the job's own, not a request's: Stop aborts it, and no A reply that fails keeps the text that had arrived, with the reason beside it; a reply that was stopped keeps its text and says so. Write at most every few tens of milliseconds when the model is fast, as the example does, since every write wakes every watcher. -The actions start and stop a job and return at once: +The server functions start and stop a job and return at once: ```ts // src/data/replies.ts @@ -143,7 +143,7 @@ export const stopReply = formAction(async (replyId: string) => { `sendMessage` stores the shopper's message and an empty reply with status `generating`, starts the job, and returns. The response closes while the job is still writing; the server-function runtime does not dispose work a function started, so the job runs to its end whether or not anyone is watching. `threads.append` is the thread half of the store, which keeps the message list and the history the model reads; the `chat` example's `db.send` is one. -`stopReply` is a Solid Router [form action](/routing/solid-router/data#mutate-with-actions), so the Stop button works as a form before JavaScript has loaded. +`stopReply` is a Solid Router [form action](/routing/solid-router/data#add-a-form-with-formaction), so the Stop button works as a form before JavaScript has loaded. `sendMessage` is a plain server function the composer calls from code; the mounted router applies its response, revalidation included, the same way it does for a form. :::caution[An orphan is a fact, not a hang] @@ -398,7 +398,7 @@ Give the job an `AbortController` the registry owns, and pass `request.signal` o ### Stop does nothing -The Stop action aborted a controller the job was not reading, or the job was started by a server that has since restarted. +The Stop button aborted a controller the job was not reading, or the job was started by a server that has since restarted. Check that `generations.start` passes `abort.signal` to the model call, and that the store marks orphaned replies `failed` on load. ### Two tabs show different replies 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 3461a1989..10f36f0db 100644 --- a/src/routes/(4)routing/(1)solid-router/(0)index.mdx +++ b/src/routes/(4)routing/(1)solid-router/(0)index.mdx @@ -235,5 +235,5 @@ Read these in order if you are new to Solid Router, or jump to the one you need: 2. [Route definitions](/routing/solid-router/route-definitions): path patterns, parameter filters, route metadata, lazy subtrees, and the file-system manifest. 3. [Nested routes and layouts](/routing/solid-router/nested-routes): an account section with its own frame that stays mounted while its pages change. 4. [Navigation and typed paths](/routing/solid-router/navigation): `paths`, `useNavigate`, search parameters with a schema, active and pending links, and leave guards. -5. [Data loading and mutations](/routing/solid-router/data): `preload`, `query`, `formAction`, optimistic updates, and what revalidates after a mutation. +5. [Data loading and mutations](/routing/solid-router/data): `preload`, `query`, mutations through server functions and actions, forms with `formAction`, optimistic updates, and what revalidates. 6. [Server rendering and hydration](/routing/solid-router/server-rendering): what the server adds when the app renders there, and how a mutation returns fresh data in one round trip. 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 6809d3c00..ce294c2e5 100644 --- a/src/routes/(4)routing/(1)solid-router/(1)setup.mdx +++ b/src/routes/(4)routing/(1)solid-router/(1)setup.mdx @@ -129,7 +129,7 @@ The other options exist for a specific situation each: | `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. | | `singleFlight` | The router's server-function data consumer should be off; it is `true` by default. | -| `actionBase` | Server actions are served from a prefix other than the default `/_server`. | +| `actionBase` | Server functions are served from a prefix other than the default `/_server`. | The router uses browser history by default on the client and the current request URL on the server, so `history` is rarely set in a web app: 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 1830d4441..16fa15d95 100644 --- a/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx +++ b/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx @@ -100,7 +100,7 @@ A number moves through history: `navigate(-1)` is back. :::tip[Let the server decide where to go] A mutation that ends in a navigation does not need `useNavigate` at all. -Return `redirect(paths.account.orders(id))` from the server function or action and the router navigates when the response arrives, in the same update as the revalidation; the [Data](/routing/solid-router/data#what-revalidates-after-a-mutation) page covers that path. +Return `redirect(paths.account.orders(id))` from the server function and the router navigates when the response arrives, in the same update as the revalidation; the [Data](/routing/solid-router/data#what-revalidates-after-a-mutation) page covers that path. ::: ## Read the location @@ -306,6 +306,6 @@ Add the schema with a transform to `Number`, and read with `useSearchParams(Rout ## 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): how links preload ahead of a click, and `redirect()` from an action. +- [Data loading and mutations](/routing/solid-router/data): how links preload ahead of a click, and `redirect()` from a mutation. - [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 c0cf18c6e..31e9e6672 100644 --- a/src/routes/(4)routing/(1)solid-router/(5)data.mdx +++ b/src/routes/(4)routing/(1)solid-router/(5)data.mdx @@ -1,13 +1,13 @@ --- title: "Data loading and mutations" version: "2.0" -description: "Start route data early with preloads, cache and share reads with query, submit mutations with form actions, show optimistic state, and control what revalidates." +description: "Start route data early with preloads, cache and share reads with query, write through server functions called from actions, add forms with formAction, show optimistic state, and control what revalidates." --- The product page from the [introduction](/routing/solid-router) fetches its product inside a memo. That works, and three things about it are not quite right yet: the request does not start until the page component runs, a `ProductCard` in a list and the `Product` page fetch the same product twice, and the **Add to cart** form has no way to tell the cart it changed. -The router's data APIs add one thing for each: `preload` starts a request before the page's component exists, `query` gives two readers one request, and `formAction` wraps a mutation so it works as a form without JavaScript and revalidates the cache when it finishes. +The router's data APIs add one thing for each: `preload` starts a request before the page's component exists, `query` gives two readers one request, and a server function called from an `action` writes, with the router revalidating the cache from its response; `formAction` puts the same mutation behind a form that works without JavaScript. This page continues the store: a product page and a cart. :::note[All of it is optional] @@ -194,14 +194,14 @@ revalidate(); // everything Keys match by prefix, so the broad key reaches every argument combination. Calling this by hand is rare; the usual trigger is a mutation finishing, covered below. -## Mutate with actions +## Mutate with a server function -A `formAction` wraps a mutation so it can be submitted as a form, tracked while it runs, and followed by revalidation. +A mutation is a server function that writes, called from an [`action`](/reference/solid-js/lifecycle-actions/action). Start with the server function: ```ts // src/data/cart.ts -import { query, formAction } from "@solidjs/router"; +import { query } from "@solidjs/router"; import { markSafeError, reload } from "@solidjs/web"; import { currentSessionId } from "./session"; @@ -210,36 +210,132 @@ export const getCart = query(async () => { return db.cart.forSession(currentSessionId()); }, "cart"); -export const addToCart = formAction(async (form: FormData) => { +export async function addToCart(productId: string, quantity = 1) { "use server"; - const productId = String(form.get("productId")); - const quantity = Number(form.get("quantity") ?? 1); if (!Number.isInteger(quantity) || quantity < 1) { throw markSafeError(new Error("Quantity must be a whole number")); } await db.cart.add(currentSessionId(), productId, quantity); return reload({ revalidate: getCart.key }); -}); +} ``` `currentSessionId()` stands for whatever your session layer provides; [Sessions and auth](/building-apps/sessions-and-auth) shows one built on a signed cookie. -Then the form: +Then call it from an action: + +```tsx +// src/pages/Product.tsx +import { action } from "solid-js"; +import { addToCart } from "../data/cart"; + +const add = action(function* (productId: string) { + yield addToCart(productId); +}); + +; +``` + +Click **Add to cart** and every reader of `getCart` shows the new line once the call returns. +Nothing here is router-specific except the response: the mounted router applies the response of every server function call, so the `reload` revalidates `getCart`, a `redirect` navigates, and in a `fullstack` project single-flight data seeds the cache. +The action makes the call one transaction, so the revalidated cart and anything the action wrote land as one update. +A call made straight from an event handler, with no action around it, gets the same revalidation; the action is what lets the page predict the result and hold it together. + +### Before the server confirms + +Write the expected result into an optimistic store before the `yield`: + +```tsx +import { For, action, createOptimisticStore } from "solid-js"; +import { getCart, addToCart } from "../data/cart"; + +function CartPanel(props: { productId: string }) { + const [cart, setCart] = createOptimisticStore( + () => getCart(), + [] as CartLine[] + ); + + const add = action(function* (productId: string) { + setCart((draft) => { + draft.push({ productId, quantity: 1, pending: true }); + }); + yield addToCart(productId); + }); + + return ( + <> + + + + ); +} +``` + +The new line appears at once with the `pending` class. +When the call returns, the router's revalidation of `getCart` and the release of the optimistic line commit together: the line is replaced by the server's copy, or disappears if the server rejected it, with no frame of the old cart in between. +[Mutations](/concepts/mutations) explains the optimistic model in depth. + +### When it fails + +A server function that throws rejects the `yield`, so the action can catch it: + +```tsx +import { action, createSignal } from "solid-js"; + +const [failure, setFailure] = createSignal(); + +const add = action(function* (productId: string, quantity: number) { + setFailure(undefined); + try { + yield addToCart(productId, quantity); + } catch (error) { + setFailure(error as Error); + } +}); +``` + +Ask for a quantity of `0` and `failure()` holds "Quantity must be a whole number". +The message reaches the browser because `addToCart` threw it through `markSafeError`. +A plain `throw new Error(...)` arrives in production as `Internal Server Error`, so database and infrastructure details do not leak; [Mutations and responses](/building-apps/server-functions/mutations-and-responses) covers `markSafeError` and the alternative of throwing validation issues with `respond()`. + +## Add a form with `formAction` + +A `formAction` puts the same mutation behind a ``. +It adds what a form needs: a URL the browser can post to before JavaScript loads, `aria-busy` while the submission runs, a record of each submission, and hooks around it. + +```ts +// src/data/cart.ts +import { formAction } from "@solidjs/router"; + +export const addToCartForm = formAction(async (form: FormData) => { + "use server"; + return addToCart( + String(form.get("productId")), + Number(form.get("quantity") ?? 1) + ); +}); +``` ```tsx // src/pages/Product.tsx - + ``` -Click **Add to cart** and the form gets `aria-busy="true"` until the call and its revalidation settle; then every reader of `getCart` shows the new line. - Read that form as a browser would. -It is a `POST` to a URL with `FormData`, and it works with JavaScript disabled: the server runs `addToCart`, sees the `reload`, and sends the browser back to the page. -With JavaScript, the router intercepts the submit, calls the form action over the server-function transport, and applies the response. +It is a `POST` to a URL with `FormData`, and it works with JavaScript disabled: the server runs the form action, sees the `reload`, and sends the browser back to the page. +With JavaScript, the router intercepts the submit, calls the form action over the server-function transport, and applies the response the same way it does for a direct call. Two rules follow from the form being the contract: @@ -273,31 +369,11 @@ export const removeFromCart = formAction( ``` `.with(...)` returns a form action whose remaining parameters start after the bound ones. -The bound values travel in the action URL, so the no-JavaScript path receives them too. - -### Client-side actions - -A form action does not have to call a server function. -A plain async function works, for a client-only mutation or a call to a third-party API: - -```ts -const savePreference = formAction(async (form: FormData) => { - localStorage.setItem("theme", String(form.get("theme"))); -}, "save-preference"); -``` - -:::caution[Name a client action that renders on the server] -A client form action rendered on the server needs the explicit name so both sides agree on the form's URL; without it the server render throws `Client Actions need explicit names if server rendered`. -A server function already has a stable URL and does not need one. -::: - -## Show what is happening - -A mutation has three moments the UI may want to reflect: while it runs, when it fails, and before the server confirms it. +The bound values travel in the form action's URL, so the no-JavaScript path receives them too. ### While it runs -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. +The router sets `aria-busy="true"` on the submitting form until the form action's result is on screen, including any revalidation or redirect it triggers. Style it in CSS and no component code is needed: ```css @@ -307,20 +383,20 @@ form[aria-busy] button { } ``` -### When it fails +### Read results with `useSubmissions` -`useSubmissions(action)` returns a reactive list of the action's completed submissions that produced a result or an error: +`useSubmissions(addToCartForm)` returns a reactive list of the form action's completed submissions that produced a result or an error: ```tsx import { Show } from "solid-js"; import { useSubmissions } from "@solidjs/router"; function AddToCartForm(props: { productId: string }) { - const submissions = useSubmissions(addToCart); + const submissions = useSubmissions(addToCartForm); const failure = () => submissions.at(-1)?.error as Error | undefined; return ( -
+ @@ -336,85 +412,43 @@ Submit a quantity of `0` and "Quantity must be a whole number" appears under the Each submission carries the `input` that was sent, the `result` or `error`, and `retry()` and `clear()`. A submission that redirected or returned nothing does not stay in the list; use `.onSettled` when you need to observe those too. -The message reaches the browser because `addToCart` threw it through `markSafeError`. -A plain `throw new Error(...)` arrives in production as `Internal Server Error`, so database and infrastructure details do not leak; [Mutations and responses](/building-apps/server-functions/mutations-and-responses) covers `markSafeError` and the alternative of throwing validation issues with `respond()`. - -### Before the server confirms +### Optimistic writes from a form -`.onSubmit` runs a callback with the action's arguments before the mutation is sent, inside the same transaction as the mutation. -Pair it with an optimistic store and the write reverts on its own when the action settles and the revalidated data lands: +A form has no action body to write in, so `.onSubmit` runs a callback with the form action's arguments before the mutation is sent, inside the same transaction: ```tsx -import { For, createOptimisticStore } from "solid-js"; -import { getCart, addToCart } from "../data/cart"; - -function CartPanel() { - const [cart, setCart] = createOptimisticStore( - () => getCart(), - [] as CartLine[] - ); - - addToCart.onSubmit((form) => { - setCart((draft) => { - draft.push({ - productId: String(form.get("productId")), - quantity: Number(form.get("quantity") ?? 1), - pending: true, - }); +addToCartForm.onSubmit((form) => { + setCart((draft) => { + draft.push({ + productId: String(form.get("productId")), + quantity: Number(form.get("quantity") ?? 1), + pending: true, }); }); - - return ( -
    - - {(line) => ( -
  • - {line.productId} × {line.quantity} -
  • - )} -
    -
- ); -} +}); ``` -The new line appears at once with the `pending` class. -When the action finishes, the `reload` revalidates `getCart`, the store reconciles the server's cart, and the optimistic line is replaced by the real one, or disappears if the server rejected it. +With `setCart` from the optimistic store in [Before the server confirms](#before-the-server-confirms), the form shows the same pending line and the same release as the action did. Registering `.onSubmit` inside a component ties the hook to that component's lifetime. The form did not change to get this. -Before hydration it is the same `POST` as in [Mutate with actions](#mutate-with-actions): the browser submits, the server runs `addToCart`, and the server renders the page again with the cart from the database. -After hydration the router intercepts the submit, runs the `.onSubmit` hooks as the first step of the action's transaction, sends the call, and reconciles the revalidated data over the prediction. -The optimistic layer is something the hydrated page adds on top of a form that already works; the server function and the HTML are the same in both cases. +Before hydration it is the same `POST` as before: the browser submits, the server runs the form action, and the server renders the page again with the cart from the database. +After hydration the router intercepts the submit, runs the `.onSubmit` hooks as the first step of the transaction, sends the call, and reconciles the revalidated data over the prediction. -[Mutations](/concepts/mutations) explains the optimistic model in depth; this is the same model with the router providing the action. +### Client-side form actions -## Call a mutation from code - -When there is no form, for example a keyboard shortcut or a drag-and-drop handler, call the server function directly: +A form action does not have to call a server function. +A plain async function works, for a client-only mutation or a call to a third-party API: ```ts -// src/data/cart.ts -export async function deleteCartLine(productId: string) { - "use server"; - await db.cart.remove(currentSessionId(), productId); - return reload({ revalidate: getCart.key }); -} -``` - -```tsx -onKeyDown={(event) => { - if (event.key === "Delete") void deleteCartLine(selectedId()); -}} +const savePreference = formAction(async (form: FormData) => { + localStorage.setItem("theme", String(form.get("theme"))); +}, "save-preference"); ``` -The mounted router applies the response the same way it does for a form: the `reload` revalidates `getCart`, a `redirect` navigates, and single-flight data seeds the cache. -No wrapper is needed for that; wrap the call in a core `action` from `solid-js` when the handler also writes optimistic state, as [Mutations](/concepts/mutations) shows. -What you give up is what the form provides: the no-JavaScript path, `aria-busy`, `useSubmissions`, and the `.onSubmit` and `.onSettled` hooks. - -:::note[`useAction` is deprecated] -Earlier releases bound a router action to code with `useAction`. -It still works, with a development warning, and has no replacement: call the server function, or a core `action` around it, directly. +:::caution[Name a client form action that renders on the server] +A client form action rendered on the server needs the explicit name so both sides agree on the form's URL; without it the server render throws `Client Actions need explicit names if server rendered`. +A server function already has a stable URL and does not need one. ::: ## What revalidates after a mutation @@ -475,11 +509,10 @@ Wrap it in `markSafeError` to send the message to the browser, or throw the vali - Declare each read once with `query(fn, "name")`; the name plus the arguments is the cache key two readers share. - Start reads in the route's `preload` with `void`, and read them through a memo in the page; `props.data` is captured once. -- Wrap a mutation in `formAction` and submit it with ``, so it works before JavaScript loads. -- Put everything the mutation needs in the form or bind it with `.with()`. -- Show progress with `form[aria-busy]`, failures with `useSubmissions`, and optimistic state with `.onSubmit` on an optimistic store. -- Throw `markSafeError` or throw `respond()` for messages the user should see. -- Call a server function directly when there is no form; the router still applies its revalidation and redirect. +- Write through a server function called from an `action`; the mounted router applies its revalidation and redirect, and an optimistic store written before the `yield` shows the result at once. +- Throw `markSafeError` or throw `respond()` for messages the user should see, and catch the rejected `yield` in the action. +- Add `formAction` when the mutation needs a `` that works before JavaScript loads; put everything it needs in the form or bind it with `.with()`. +- On a form, show progress with `form[aria-busy]`, failures with `useSubmissions`, and optimistic state with `.onSubmit`. - Narrow revalidation with `reload`, `respond`, or `redirect` options; without keys, every query revalidates after any `POST` server function, and `revalidate: ""` opts out. ## Next steps diff --git a/src/routes/(4)routing/(1)solid-router/(6)server-rendering.mdx b/src/routes/(4)routing/(1)solid-router/(6)server-rendering.mdx index f6243e007..402a161b6 100644 --- a/src/routes/(4)routing/(1)solid-router/(6)server-rendering.mdx +++ b/src/routes/(4)routing/(1)solid-router/(6)server-rendering.mdx @@ -58,9 +58,9 @@ Adoption also works for reads that happen after hydration finishes, so a lazy ch Submit a form backed by a `formAction`, or call a server function from a click handler, on a page that also reads a `query`. Without the server-side collector, the browser makes two requests: the mutation, then the revalidation fetch when the router reloads the affected queries. -With it, the browser makes one: the mutation response carries the fresh query values, and the router seeds its cache from them before the action's caller even receives the return value. +With it, the browser makes one: the mutation response carries the fresh query values, and the router seeds its cache from them before the caller even receives the return value. -![Two sequences between browser and server. Without the collector: the action request, its result, then a second request that reloads the affected queries. With the collector: one action request whose response carries the fresh query values; the router seeds its cache, then resolves the call.](/images/diagrams/single-flight-mutation.svg) +![Two sequences between browser and server. Without the collector: the mutation request, its result, then a second request that reloads the affected queries. With the collector: one mutation request whose response carries the fresh query values; the router seeds its cache, then resolves the call.](/images/diagrams/single-flight-mutation.svg) The `fullstack` template enables this in two files. The first registers the router as the collector: @@ -100,22 +100,22 @@ To turn the protocol off, set `singleFlight: false` in `createRouter`. Without a client consumer the transport does not send the single-flight header and the server does not run collection. An app with its own server-function handler passes `collectFlightData` to `handleServerFunctionRequest` per request instead of using the `configure` module. -If the default server-function endpoint `/_server` is changed in the Vite plugin, set the router's `actionBase` to match so action URLs still reach it. +If the default server-function endpoint `/_server` is changed in the Vite plugin, set the router's `actionBase` to match so form action URLs still reach it. ## Forms without JavaScript -Disable JavaScript and submit the cart form from the [Data](/routing/solid-router/data#mutate-with-actions) page. +Disable JavaScript and submit the cart form from the [Data](/routing/solid-router/data#add-a-form-with-formaction) page. It still works. -The action's URL points at the server-function handler. +The form action's URL points at the server-function handler. The handler sees a plain form `POST` with no client runtime behind it, runs the server function, and instead of returning a payload the browser could not use, redirects back to the page with the outcome stored in a one-shot flash cookie. When the router renders that page on the server, it clears and decodes the cookie into the same submission records that `useSubmissions` exposes, so a validation error from a no-JavaScript submit shows up in the same `

` as a scripted one. -The router also covers the gap between the page loading and the action's module arriving. -If a form is submitted before the code that defines its action has been loaded, the router recognizes the server-action URL, loads the submit path on demand, and sends the form through the server-function transport as usual. +The router also covers the gap between the page loading and the form action's module arriving. +If a form is submitted before the code that defines its form action has been loaded, the router recognizes the server-function URL, loads the submit path on demand, and sends the form through the server-function transport as usual. -:::note[Only server-backed actions have a no-JavaScript path] -A client-only action has no URL the server can run, so it needs its module loaded before the form is submitted. +:::note[Only server-backed form actions have a no-JavaScript path] +A client-only form action has no URL the server can run, so it needs its module loaded before the form is submitted. [Progressive enhancement](/building-apps/server-functions/progressive-enhancement) describes what the core runtime owns in this exchange and what the router adds. ::: diff --git a/src/routes/(4)routing/(2)tanstack/(0)index.mdx b/src/routes/(4)routing/(2)tanstack/(0)index.mdx index be743fedb..8164d2649 100644 --- a/src/routes/(4)routing/(2)tanstack/(0)index.mdx +++ b/src/routes/(4)routing/(2)tanstack/(0)index.mdx @@ -164,7 +164,7 @@ Wrap the call in an arrow, as the route above does. The template uses that declaration to scope single-flight collection; a mutation that returns nothing gets the unscoped behavior, which re-runs the page's loaders and ships everything they produce. Forms here are scripted (`onSubmit` calling `mutate`), the usual TanStack shape. -For forms that must work without JavaScript, the `fullstack` shape's Solid Router actions are the supported path; [Progressive enhancement](/building-apps/server-functions/progressive-enhancement) describes what the server-function runtime does for an unscripted `POST` regardless of router. +For forms that must work without JavaScript, the `fullstack` shape's Solid Router form actions are the supported path; [Progressive enhancement](/building-apps/server-functions/progressive-enhancement) describes what the server-function runtime does for an unscripted `POST` regardless of router. ## What the server does per request diff --git a/src/routes/(5)guides/(4)forms.mdx b/src/routes/(5)guides/(4)forms.mdx index cb59cf1b7..3cec8ee4d 100644 --- a/src/routes/(5)guides/(4)forms.mdx +++ b/src/routes/(5)guides/(4)forms.mdx @@ -20,7 +20,6 @@ The guide assumes a `fullstack` project with [server functions](/building-apps/s :::pitfall[`formAction`, not `action`] This guide uses `formAction` from `@solidjs/router`. It wraps a function so a form can submit to it and the router can track the submission. -Earlier router releases called it `action`; that name still works as a deprecated alias with a development warning. `solid-js` exports its own [`action`](/reference/solid-js/lifecycle-actions/action); that one runs a generator as a reactive transaction and is covered in [Mutations](/concepts/mutations). `` fails at the type level because the core `action` does not produce a URL. @@ -243,7 +242,7 @@ export default function AddressPage() { Submit the form with an empty name and the message appears under the name field, with JavaScript on or off. The `` element and its inputs are the same as in pass 1; only the message elements were added. -`useSubmissions(submitAddress)` returns a reactive array of this action's settled submissions, each with the `input` that was sent and either a `result` or an `error`. +`useSubmissions(submitAddress)` returns a reactive array of this form action's settled submissions, each with the `input` that was sent and either a `result` or an `error`. When `saveAddress` throws the `respond()` envelope, the carried value becomes `submission.error`, which is where `issues()` reads the field messages. A submission that returned a redirect does not stay in the list. `issues()` is a plain derived function read from JSX, following the rule from the [Reactivity](/concepts/reactivity) page. @@ -301,9 +300,9 @@ export async function addAddress(form: FormData) { ``` `toIssues` is the loop from pass 2 moved into a helper, and `currentCustomerId()` stands for the session read from [Sessions and auth](/building-apps/sessions-and-auth). -`reload({ revalidate: getAddresses.key })` tells the router which query to refetch when the action completes. +`reload({ revalidate: getAddresses.key })` tells the router which query to refetch when the form action completes. -The page reads the list through an optimistic store and pushes the submitted fields into it from the action's `.onSubmit` hook: +The page reads the list through an optimistic store and pushes the submitted fields into it from the form action's `.onSubmit` hook: ```tsx // src/routes/checkout/address.tsx @@ -357,7 +356,7 @@ export default function AddressPage() { Click **Save address** and the new line appears at the bottom of the list with the `pending` class before any request has finished. When `addAddress` returns, the router revalidates `getAddresses`, the store reconciles the server's list, and the unsaved line is replaced by the saved one with its real `id`. -Post a bad postal code and the line disappears when the `400` arrives, because an optimistic write is an overlay that Solid discards when the action settles, on success and on failure alike; the inline message from pass 3 explains why. +Post a bad postal code and the line disappears when the `400` arrives, because an optimistic write is an overlay that Solid discards when the form action settles, on success and on failure alike; the inline message from pass 3 explains why. `.onSubmit` receives the action's arguments, here the `FormData`, and runs as the first step of the action's transaction, so the write is held with the mutation instead of committing on its own. [Before the server confirms](/routing/solid-router/data#before-the-server-confirms) covers the hook, and [Mutations](/concepts/mutations) explains why the overlay needs no rollback code. @@ -468,9 +467,9 @@ Use `throw respond(value, { status })` for structured failures. - Leave inputs uncontrolled unless a field needs feedback while typing; the browser assembles the `FormData` on submit. - Validate on the server with a schema; browser attributes are a convenience anyone can bypass with `curl`. - Throw `respond(value, { status: 400 })` for a validation failure; a plain `Error` reaches production clients as `Internal Server Error`. -- Read failures from `useSubmissions(action)`, where the envelope's value is `submission.error`. +- Read failures from `useSubmissions(submitAddress)`, where the envelope's value is `submission.error`. - Style the pending state from `form[aria-busy]`; the router sets and clears the attribute. -- Push the submitted fields into a `createOptimisticStore` from the action's `.onSubmit` hook to show the result at once; the overlay is discarded when the action settles and the revalidated list replaces it. +- Push the submitted fields into a `createOptimisticStore` from the form action's `.onSubmit` hook to show the result at once; the overlay is discarded when the form action settles and the revalidated list replaces it. - Every layer after pass 1 exists only on the hydrated page; the same form still posts, and the server renders the page again, without JavaScript. ## Next steps diff --git a/src/routes/(5)guides/(7)protected-routes.mdx b/src/routes/(5)guides/(7)protected-routes.mdx index c31c70b8a..aa76da630 100644 --- a/src/routes/(5)guides/(7)protected-routes.mdx +++ b/src/routes/(5)guides/(7)protected-routes.mdx @@ -347,7 +347,7 @@ Accept only paths that start with a single `/` and reject `//` and `/\`; run tha - Build the sign-in target with `paths["sign-in"]({ next })`; the search object encodes the path. - Validate `next` inside the server function: a single leading `/`, no `//`, no `/\`, and not the sign-in page itself. - Return a redirect from middleware for full page requests under `/account`, and keep the function checks, because middleware does not see `/_server` or client-side navigations. -- Read `getCurrentUser()` through `query` in the header so the server result hydrates and a sign-out action refreshes it. +- Read `getCurrentUser()` through `query` in the header so the server result hydrates and a sign-out form action refreshes it. ## Next steps diff --git a/src/routes/(7)glossary.mdx b/src/routes/(7)glossary.mdx index 55f0ab870..acd51c292 100644 --- a/src/routes/(7)glossary.mdx +++ b/src/routes/(7)glossary.mdx @@ -108,9 +108,9 @@ See [Signals](/concepts/reactivity#signals) and the [`untrack` reference](/refer ### Action A generator function wrapped in `action` from `solid-js` that runs a mutation as one transaction: each `yield` waits for its promise and then restores the transaction, so the write before the `yield` and the refresh after it belong to the same update, and a plain `await` would leave it. -Solid Router's `formAction`, formerly also named `action`, adds a form URL and submissions on top of a server function. +Solid Router's `formAction` adds a form URL and submissions on top of a server function. -See [Move the cart to the server](/concepts/mutations#move-the-cart-to-the-server) and [Mutate with actions](/routing/solid-router/data#mutate-with-actions). +See [Move the cart to the server](/concepts/mutations#move-the-cart-to-the-server) and [Mutate with a server function](/routing/solid-router/data#mutate-with-a-server-function). ### affects @@ -172,7 +172,7 @@ See [Move the cart to the server](/concepts/mutations#move-the-cart-to-the-serve ### Revalidation Marking cached reads as stale and re-running the ones that are being read. -A server function returns `reload`, `respond`, or `redirect` with a `revalidate` option to name the keys; Solid Router matches them against its `query` cache when the action completes, and with no keys every query revalidates. +A server function returns `reload`, `respond`, or `redirect` with a `revalidate` option to name the keys; Solid Router matches them against its `query` cache when the mutation completes, and with no keys every query revalidates. See [Revalidate](/routing/solid-router/data#revalidate) and [Request revalidation](/building-apps/server-functions/mutations-and-responses#request-revalidation). @@ -373,7 +373,7 @@ See [Same-origin protection](/building-apps/server-functions/arguments-and-secur A form that posts to a server function's URL works before the client bundle loads, and the router takes it over after hydration. -See [Post a form through a router action](/building-apps/server-functions/progressive-enhancement#post-a-form-through-a-router-action). +See [Post a form through a form action](/building-apps/server-functions/progressive-enhancement#post-a-form-through-a-form-action). ### Projection diff --git a/src/routes/reference/(1)solid-js/(0)index.mdx b/src/routes/reference/(1)solid-js/(0)index.mdx index 279bf0066..3706747a7 100644 --- a/src/routes/reference/(1)solid-js/(0)index.mdx +++ b/src/routes/reference/(1)solid-js/(0)index.mdx @@ -22,6 +22,7 @@ Values that change over time and the computations that follow them. - [`createSignal`](/reference/solid-js/reactivity/create-signal) creates a readable, writable value. - [`createMemo`](/reference/solid-js/reactivity/create-memo) derives a cached value from other reactive reads, including async ones. +- [`createDeferred`](/reference/solid-js/reactivity/create-deferred) derives an async value that keeps showing its last answer while a refetch is in flight. - [`createEffect`](/reference/solid-js/reactivity/create-effect) runs side effects after tracked values change. - [`createOptimistic`](/reference/solid-js/reactivity/create-optimistic) creates a signal whose writes inside an action are tentative. - [`isPending`](/reference/solid-js/reactivity/is-pending) reports that a new answer is on the way; [`latest`](/reference/solid-js/reactivity/latest) reads it early. diff --git a/src/routes/reference/(1)solid-js/(1)reactivity/create-deferred.mdx b/src/routes/reference/(1)solid-js/(1)reactivity/create-deferred.mdx new file mode 100644 index 000000000..ec8129c3e --- /dev/null +++ b/src/routes/reference/(1)solid-js/(1)reactivity/create-deferred.mdx @@ -0,0 +1,71 @@ +--- +title: "createDeferred" +category: "Reactivity" +use_cases: "reactivity api, createdeferred usage" +tags: + - "create" + - "deferred" + - "reactivity" + - "reference" + - "api" + - "v2" +version: "2.0" +description: "Creates an async memo that may lag, but never leads: after its first answer, a refetch holds nothing — readers keep the previous answer until the new one lands, and `isPending` reports the refetch." +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. */} + +Creates an async memo that may lag, but never leads: after its first +answer, a refetch holds nothing — readers keep the previous answer until +the new one lands, and `isPending` reports the refetch. See +`createDeferred` in `@solidjs/signals`. Hydrates like `createMemo`: the +server's answer is its first. + +## Import + +```ts +import { createDeferred } from "solid-js"; +``` + +## Type signature + +```ts +function createDeferred( + compute: ComputeFunction, T>, + options: MemoOptions & { loadingValue: T } +): SourceAccessor; +function createDeferred( + compute: ComputeFunction, T>, + options?: MemoOptions +): SourceAccessor; +``` + +## Parameters + +### `compute` + +- **Type:** `ComputeFunction, T> | ComputeFunction, T>` + +### `options` + +- **Type:** `MemoOptions & { loadingValue: T } | MemoOptions` +- Optional + +## Examples + +```tsx +const rows = createDeferred(() => fetchRows(store.period)); + +

rows()) }}> + {(row) => } +
; +``` + +## Learn more + +- [Reactivity](/concepts/reactivity) +- [Async reactivity](/concepts/async-reactivity) +- [Debugging reactivity](/guides/debugging-reactivity) diff --git a/src/routes/reference/(1)solid-js/(1)reactivity/create-memo.mdx b/src/routes/reference/(1)solid-js/(1)reactivity/create-memo.mdx index cc8f4f36f..cdb030437 100644 --- a/src/routes/reference/(1)solid-js/(1)reactivity/create-memo.mdx +++ b/src/routes/reference/(1)solid-js/(1)reactivity/create-memo.mdx @@ -132,6 +132,7 @@ Also used in combination with `SignalOptions` for writable memos interface MemoOptions { id?: string; name?: string; + loc?: string; transparent?: boolean; _plumbing?: boolean; equals?: false | ((prev: T, next: T) => boolean); @@ -152,6 +153,13 @@ Stable identifier for the owner hierarchy Debug name (dev mode only) +**`loc`** — `string` + +Source location (`file:line:col`, root-relative) written by the +compiler's `sourceNames` `locations` kind beside `name`; dev and observe +builds keep it for console reports and diagnostic events, production +ignores it. Not meant to be written by hand. + **`transparent`** — `boolean` Advanced (integration tier). When true, the memo is invisible to the @@ -263,6 +271,7 @@ Options for plain signals created with `createSignal(value)` or `createOptimisti ```ts interface SignalOptions { name?: string; + loc?: string; equals?: false | ((prev: T, next: T) => boolean); ownedWrite?: boolean; unobserved?: () => void; @@ -275,6 +284,13 @@ interface SignalOptions { Debug name (dev mode only) +**`loc`** — `string` + +Source location (`file:line:col`, root-relative) written by the +compiler's `sourceNames` `locations` kind beside `name`; dev and observe +builds keep it for console reports and diagnostic events, production +ignores it. Not meant to be written by hand. + **`equals`** — `false | ((prev: T, next: T) => boolean)` Custom equality function, or `false` to always notify subscribers. 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 cf8379355..4324c7a6b 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 @@ -163,6 +163,7 @@ Also used in combination with `SignalOptions` for writable memos interface MemoOptions { id?: string; name?: string; + loc?: string; transparent?: boolean; _plumbing?: boolean; equals?: false | ((prev: T, next: T) => boolean); @@ -183,6 +184,13 @@ Stable identifier for the owner hierarchy Debug name (dev mode only) +**`loc`** — `string` + +Source location (`file:line:col`, root-relative) written by the +compiler's `sourceNames` `locations` kind beside `name`; dev and observe +builds keep it for console reports and diagnostic events, production +ignores it. Not meant to be written by hand. + **`transparent`** — `boolean` Advanced (integration tier). When true, the memo is invisible to the @@ -294,6 +302,7 @@ Options for plain signals created with `createSignal(value)` or `createOptimisti ```ts interface SignalOptions { name?: string; + loc?: string; equals?: false | ((prev: T, next: T) => boolean); ownedWrite?: boolean; unobserved?: () => void; @@ -306,6 +315,13 @@ interface SignalOptions { Debug name (dev mode only) +**`loc`** — `string` + +Source location (`file:line:col`, root-relative) written by the +compiler's `sourceNames` `locations` kind beside `name`; dev and observe +builds keep it for console reports and diagnostic events, production +ignores it. Not meant to be written by hand. + **`equals`** — `false | ((prev: T, next: T) => boolean)` Custom equality function, or `false` to always notify subscribers. diff --git a/src/routes/reference/(1)solid-js/(1)reactivity/is-pending.mdx b/src/routes/reference/(1)solid-js/(1)reactivity/is-pending.mdx index e170e90e6..7f4378685 100644 --- a/src/routes/reference/(1)solid-js/(1)reactivity/is-pending.mdx +++ b/src/routes/reference/(1)solid-js/(1)reactivity/is-pending.mdx @@ -42,10 +42,20 @@ Expression to probe. Read the values in question inside it. ## Return value -`true` while a read inside `fn` depends on a change that has not committed yet. +`true` while a read inside `fn` depends on a change that has not committed yet ## Examples +```ts +const [query, setQuery] = createSignal(""); +const results = createMemo(() => searchProducts(query())); +const stale = createMemo(() => isPending(results)); + +setQuery("shoes"); +// stale() is true until the new results land, while results() keeps +// serving the previous list — dim it instead of showing a spinner. +``` + ### Dim stale results while a new answer loads ```tsx diff --git a/src/routes/reference/(1)solid-js/(1)reactivity/untrack.mdx b/src/routes/reference/(1)solid-js/(1)reactivity/untrack.mdx index 628a03a5e..2cf87a741 100644 --- a/src/routes/reference/(1)solid-js/(1)reactivity/untrack.mdx +++ b/src/routes/reference/(1)solid-js/(1)reactivity/untrack.mdx @@ -23,6 +23,16 @@ subscribe the current scope. Returns whatever `fn` returns. Use `untrack` inside a memo or effect when you need to read a signal once without making the surrounding computation depend on its future changes. +`untrack` stops dependency tracking, not async settlement. Reading an +uninitialized async source still suspends the owning computation; it runs +again when that source first resolves. Afterward, the untracked read is a +snapshot and does not subscribe the computation to later changes. + +If the first read must return immediately, give the async source a +`loadingValue` before reading it with `untrack`. `isPending` and `latest` +inspect updates to an existing value; neither supplies a missing initial +value. + Pass a `strictReadLabel` string to enable a dev-mode warning: any reactive read inside `fn` that isn't inside a nested tracking scope will log a warning naming the label. @@ -68,6 +78,9 @@ createEffect( log(snapshot); } ); + +const color = createMemo(() => fetchColor(), { loadingValue: "gray" }); +const initialColor = untrack(color); // "gray" while the fetch is unresolved ``` ### Take a non-suspending snapshot of an async source 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 f7613a1b9..3d1e63d46 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 @@ -253,6 +253,7 @@ Options shared by all store primitives. ```ts interface StoreOptions { name?: string; + loc?: string; shallow?: boolean; } ``` @@ -265,6 +266,15 @@ Debug name (dev and observe builds). Property nodes are labelled `.` in attribution output (`todos.title`); a derived store's projection node carries the name itself. +#### `loc` + +- **Type:** `string` + +Source location (`file:line:col`, root-relative) written by the +compiler's `sourceNames` `locations` kind; dev and observe builds keep it +for console reports and diagnostic events, production ignores it. Not +meant to be written by hand. + #### `shallow` - **Type:** `boolean` 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 1d48b6afa..395e695f2 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 @@ -85,14 +85,7 @@ interface Dev { - **Type:** `void` -Console face of an emitted event — see `reportDiagnostic`: one entry -per finding — the message, the `in` line built from `ownerPath` -(`in › › effect`), the `at` line built from `loc` -(`at src/App.tsx:18:3`; in a browser `at http://localhost:5173/src/App.tsx:18:3`, -which Chrome and Firefox link to the Sources panel — a path that leaves -the root prints as-is), the once-per-code footer, and, for a binding -effect the rendering runtime tagged, the element it writes as a second -console argument. +Console face of an emitted event — see `reportDiagnostic`. #### `guideUrl` @@ -100,11 +93,11 @@ console argument. The URL of `code`'s page in the docs — what the finding means and how to fix it, one page per code -([`/reference/solid-js/diagnostics/`](/reference/solid-js/diagnostics), -slug = the code lowercased with `-` for `_`). The one place the URL is -built: the console footer prints it and the performance tracks' Insights -link (`learnMoreUrl`) reads it, so both name the same page. Dev-tier: it -is guidance for a developer, and a URL string on a retained object is a +(`https://v2.solidjs.com/reference/solid-js/diagnostics/`, slug = +the code lowercased with `-` for `_`). The one place the URL is built: +the console footer prints it and the performance tracks' Insights link +(`learnMoreUrl`) reads it, so both name the same page. Dev-tier: it is +guidance for a developer, and a URL string on a retained object is a cost every observe build would pay. ### `DevHooks` @@ -120,6 +113,7 @@ interface DevHooks { value: any, prev: any ) => void; + onRead?: (node: object) => void; } ``` @@ -139,6 +133,10 @@ interface DevHooks { - **Type:** `(state: any, property: PropertyKey, value: any, prev: any) => void` +#### `onRead` + +- **Type:** `(node: object) => void` + ### `DiagnosticCapture` ```ts @@ -230,7 +228,6 @@ type DiagnosticCode = | "UNRECOGNIZED_INSERT_VALUE" | "UNSCOPED_HOLE_ALLOCATED_IDS" | "LOWERCASE_EVENT_ATTRIBUTE" - | "BINDING_SLOT_POSITION" | "FRAME_MARKER_CORRUPTED" | "DYNAMIC_ASYNC_COMPONENT"; ``` @@ -301,11 +298,11 @@ is usually the finding itself). - **Type:** `string` Source location of the innermost located node enclosing the subject -(`file:line:col`, relative to the project root — the compiler's -`sourceNames` `locations` kind): the subject's own `loc`, else the -nearest owner's up the chain (a component root carries its JSX site). -Absent when nothing on the chain was compiled with locations. A host -event that sets it keeps it. +(`file:line:col`, root-relative — the compiler's `sourceNames` +`locations` kind): the subject's own `loc`, else the nearest owner's up +the chain (a component root carries its JSX site). Absent when nothing +on the chain was compiled with locations. A host event that sets it +keeps it. #### `data` 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 4e2571647..85e51a4fd 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 @@ -56,6 +56,8 @@ interface AttributionSlot { withInteraction(ref: InteractionRef, fn: () => T): T; withOrigin(ref: NavigationRef, fn: () => T): T; currentOrigin(): ChangeOrigin | undefined; + markFlight(flight: object, startedAt?: number): void; + flightOrigin(flight: object): number | undefined; } ``` @@ -102,6 +104,27 @@ own beside the engine's records: `@solidjs/web` stamps its `"call"` record with this, so a server-function call joins the interaction or navigation it ran for by the identity of the object, not by time. +#### `markFlight` + +- **Type:** `void` + +Stamps a flight (a promise or async iterable a computation will await) +with when its work really started, before anything reads it. A route +preloader, query cache or `lazy()` calls this on what it hands out (on +the wrapper it mints, with the original kickoff time), so a reader that +meets it later is judged against the real start: work already running +when the previous wait landed is parallel, not a waterfall link. The +client engine's `ASYNC_WATERFALL` and the server's +`SSR_BOUNDARY_WATERFALL` both read these stamps. Earliest wins. Works +with no engine installed. `startedAt` is on the `performance.now()` +clock and defaults to now. + +#### `flightOrigin` + +- **Type:** `number | undefined` + +The earliest start `markFlight` declared for `flight`, if any. + ### `BoundaryEvent` One `` boundary that WAITED during a server render — discovered @@ -604,7 +627,6 @@ type DiagnosticCode = | "UNRECOGNIZED_INSERT_VALUE" | "UNSCOPED_HOLE_ALLOCATED_IDS" | "LOWERCASE_EVENT_ATTRIBUTE" - | "BINDING_SLOT_POSITION" | "FRAME_MARKER_CORRUPTED" | "DYNAMIC_ASYNC_COMPONENT"; ``` @@ -675,11 +697,11 @@ is usually the finding itself). - **Type:** `string` Source location of the innermost located node enclosing the subject -(`file:line:col`, relative to the project root — the compiler's -`sourceNames` `locations` kind): the subject's own `loc`, else the -nearest owner's up the chain (a component root carries its JSX site). -Absent when nothing on the chain was compiled with locations. A host -event that sets it keeps it. +(`file:line:col`, root-relative — the compiler's `sourceNames` +`locations` kind): the subject's own `loc`, else the nearest owner's up +the chain (a component root carries its JSX site). Absent when nothing +on the chain was compiled with locations. A host event that sets it +keeps it. #### `data` @@ -748,7 +770,7 @@ Records an event on the channel from outside the reactive core — a host runtime reporting its own findings (hydration mismatches, server render faults) so consumers see one stream. `subject` locates it like the internal sites do; a host whose owners are not signals' owners passes -`ownerPath` (and `loc`) on the event instead and they are used as-is. +`ownerPath` on the event instead and it is used as-is. ### `DiagnosticSeverity` 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 a716e8667..7876746e7 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 @@ -110,9 +110,7 @@ Options for `configureServerFunctionsClient`. interface ServerFunctionsClientConfig { endpoint?: string; codec?: JSONCodecOptions; - fetch?: - | ((address: string, init: RequestInit) => Response | Promise) - | null; + fetch?: ServerFunctionsSend | null; prepareRequest?: PrepareRequestHook; responseHandler?: { capture?(info: { id: string; meta: unknown }): unknown; @@ -124,6 +122,7 @@ interface ServerFunctionsClientConfig { args: unknown[]; context: unknown; retry(): Promise; + startedAt?: number; } ): unknown; intercept?(info: { @@ -172,16 +171,19 @@ cookie, and `csrf.allowCredentials` on the server. Codec options (extra plugins etc.) for encoding arguments and decoding results — must match the server's. Stored in the shared layer, so -`decodeResponse` sees them too. +`decodeResponse` sees them too. Arguments never carry `Response` or +`Request` values, whatever plugins are configured: the server refuses +them, and the rich-args encoder rejects the call before sending. #### `fetch` -- **Type:** `((address: string, init: RequestInit) => Response | Promise) | null` +- **Type:** `ServerFunctionsSend | null` Sends every server-function request — retries, telemetry, a test -double, or an app's own route. Always called as `(address, init)`, the +double, or an app's own route. Called as `(address, init)`, the address relative to the document as the global one receives it, so -`parseServerFunctionActionUrl` reads the id back out for telemetry. `null` +`parseServerFunctionActionUrl` reads the id back out for telemetry (a +trailing third argument is the transport's own; ignore it). `null` restores the global. ```ts @@ -231,7 +233,7 @@ 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; 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; }` +- **Type:** `{ capture?(info: { id: string; meta: unknown }): unknown; handle( response: Response, ctx: { id: string; meta: unknown; args: unknown[]; context: unknown; retry(): Promise; startedAt?: number; } ): 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 @@ -250,3 +252,14 @@ Encoder for argument lists JSON can't carry faithfully. JSON-safe args always go as plain JSON (no codec in the bundle); anything else throws unless this is set. Installed by `enableRichArguments()` from the rich-args entry — set directly only for custom wire encodings. + +### `ServerFunctionsSend` + +How the transport sends a request: `fetch`'s shape, as the runtime calls it. + +```ts +type ServerFunctionsSend = ( + address: string, + init: RequestInit +) => Response | Promise; +``` diff --git a/src/routes/reference/(2)solid-web/(5)server-functions/host-configuration.mdx b/src/routes/reference/(2)solid-web/(5)server-functions/host-configuration.mdx index c4ad56499..fb6f8838d 100644 --- a/src/routes/reference/(2)solid-web/(5)server-functions/host-configuration.mdx +++ b/src/routes/reference/(2)solid-web/(5)server-functions/host-configuration.mdx @@ -174,7 +174,7 @@ cover every hop belongs on `configureServerFunctionsServer`'s This request's server error hook, ahead of `configureServerErrors`' (see `ServerErrorHook` in `@solidjs/web`): the function's throw (`handling: "thrown"`) and a failure escaping through its result graph -(`"channel"`), once per error, before the wire policy applies. Entry-only +(`"channel"`), once per error per request, before the wire policy applies. Entry-only like `wrapInvocation`: a direct call the body makes during a render reports through the ambient hook. @@ -501,7 +501,11 @@ for a cross-origin client, answered with CORS (see Codec options (extra plugins etc.) for decoding arguments and encoding results — must match the client's. Stored in the shared layer, so -`decodeResponse` sees them too. When `serializeErrorStacks` is omitted, +`decodeResponse` sees them too. One asymmetry is fixed: arguments are +client input and never decode `Response` or `Request` values, whatever +plugins are configured — a call carrying one is refused with `400`. +Results keep the full set. Every plugin added here is a type a client +can make the server construct. When `serializeErrorStacks` is omitted, the server-function boundary defaults it from this module's compiled development variant. diff --git a/src/routes/reference/(2)solid-web/(5)server-functions/metadata.mdx b/src/routes/reference/(2)solid-web/(5)server-functions/metadata.mdx index 942e2386f..e53dc5954 100644 --- a/src/routes/reference/(2)solid-web/(5)server-functions/metadata.mdx +++ b/src/routes/reference/(2)solid-web/(5)server-functions/metadata.mdx @@ -151,6 +151,11 @@ over earlier ones. interface ServerFunctionMetadata { readonly method?: "GET" | "POST"; readonly name?: string; + readonly fetch?: ( + address: string, + init: RequestInit, + next: (address: string, init: RequestInit) => Response | Promise + ) => Response | Promise; readonly [key: string]: unknown; } ``` @@ -170,3 +175,24 @@ from the compiled function's source name (dev tooling — inspectors, logs). Dev-only: production builds emit no name. Not unique and not an identity key — use `id` for identity. Seeded as a default: an explicit `withMeta` write wins. + +#### `fetch` + +- **Type:** `( address: string, init: RequestInit, next: (address: string, init: RequestInit) => Response | Promise ) => Response | Promise` + +This function's own send, client side: the transport calls it instead +of the configured `fetch` for every request the function makes, with +the configured send (the app's `fetch`, or the global) as `next`. For +an integration that answers a declared function's calls itself — a +prerendered build's static artifacts, a cache — while the app's +transport `fetch` stays the app's: pass on what you don't answer +through `next`. The contract is `fetch`'s (see +`ServerFunctionsClientConfig.fetch`); hydration adopts a +server-rendered read only if the send reaches the global `fetch` in the +same tick. + +```ts +withMeta(getPosts, { + fetch: (address, init, next) => cached(address) ?? next(address, init), +}); +``` 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 3e43e129d..e8353a8b2 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 @@ -95,13 +95,20 @@ interface ServerErrorContext extends Omit { ### `ServerErrorHook` The server error hook (see `configureServerErrors`): every failure the -server runtime handles or fails on, once per error object, with where it -was met. Return the value the client should receive in place of the error -— rendered into the fallback, serialized for hydration, sent as the RPC -error — or nothing for the default policy (a generic `Error` outside the -dev build, the error itself in it; `markSafeError` still passes through). +server runtime handles or fails on, once per error object per `kind` per +request (per render when no request is in scope — never once per +process: an error object one request met and another meets again is +heard by both), with where it was met. A direct server-function call that +throws during SSR and the `` that contains it are heard as both: +`server-function` (the call failed), then `render` (the render fell back), +so `kind: "render"` alone answers whether a render failed. Return the value the client should receive in +place of the error — rendered into the fallback, serialized for +hydration, sent as the RPC error — or nothing for the default policy (a +generic `Error` outside the dev build, the error itself in it; +`markSafeError` still passes through). A returned value is taken as intended client-facing content and is not -sanitized again. Ignored for `handling: "failed"`, which has no wire. +sanitized again. Ignored for `handling: "failed"`, which has no wire, and +once an earlier kind's sight gave one: every road carries the first. One road a mapping does not reach: a rejected async source's serialized rejection is encoded the moment the source rejects, ahead of the boundary diff --git a/src/routes/reference/(2)solid-web/(6)request-response/redirect.mdx b/src/routes/reference/(2)solid-web/(6)request-response/redirect.mdx index 98d4a2bf1..95563f3a4 100644 --- a/src/routes/reference/(2)solid-web/(6)request-response/redirect.mdx +++ b/src/routes/reference/(2)solid-web/(6)request-response/redirect.mdx @@ -124,8 +124,8 @@ interface ResponseHelperInit extends ResponseInit { The cache keys the mutation invalidated, sent as the `X-Revalidate` header for the client's integrations to apply. Three declarations, three scopes: omitted sends no header — the host's default (Solid Router -revalidates everything after an action; a router without that -convention reloads only what it owns); an empty list sends an empty -header — nothing, explicitly; `REVALIDATE_ALL` (`"*"`) — every entry, +revalidates everything after any mutation, form action or direct call; +a router without that convention reloads only what it owns); an empty +list (or `""`) sends an empty header — nothing, explicitly; `REVALIDATE_ALL` (`"*"`) — every entry, whatever the host's default. How named keys are matched (prefixes, namespaces) is each integration's business. diff --git a/src/routes/reference/(2)solid-web/(6)request-response/reload.mdx b/src/routes/reference/(2)solid-web/(6)request-response/reload.mdx index e94bebd54..25c4de6e3 100644 --- a/src/routes/reference/(2)solid-web/(6)request-response/reload.mdx +++ b/src/routes/reference/(2)solid-web/(6)request-response/reload.mdx @@ -73,8 +73,8 @@ interface ResponseHelperInit extends ResponseInit { The cache keys the mutation invalidated, sent as the `X-Revalidate` header for the client's integrations to apply. Three declarations, three scopes: omitted sends no header — the host's default (Solid Router -revalidates everything after an action; a router without that -convention reloads only what it owns); an empty list sends an empty -header — nothing, explicitly; `REVALIDATE_ALL` (`"*"`) — every entry, +revalidates everything after any mutation, form action or direct call; +a router without that convention reloads only what it owns); an empty +list (or `""`) sends an empty header — nothing, explicitly; `REVALIDATE_ALL` (`"*"`) — every entry, whatever the host's default. How named keys are matched (prefixes, namespaces) is each integration's business. 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 2f75af0ef..26a4c6023 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 @@ -141,9 +141,9 @@ interface ResponseHelperInit extends ResponseInit { The cache keys the mutation invalidated, sent as the `X-Revalidate` header for the client's integrations to apply. Three declarations, three scopes: omitted sends no header — the host's default (Solid Router -revalidates everything after an action; a router without that -convention reloads only what it owns); an empty list sends an empty -header — nothing, explicitly; `REVALIDATE_ALL` (`"*"`) — every entry, +revalidates everything after any mutation, form action or direct call; +a router without that convention reloads only what it owns); an empty +list (or `""`) sends an empty header — nothing, explicitly; `REVALIDATE_ALL` (`"*"`) — every entry, whatever the host's default. How named keys are matched (prefixes, namespaces) is each integration's business. diff --git a/src/routes/reference/(3)solid-router/(4)data.mdx b/src/routes/reference/(3)solid-router/(4)data.mdx index 52f878657..ff06d09b7 100644 --- a/src/routes/reference/(3)solid-router/(4)data.mdx +++ b/src/routes/reference/(3)solid-router/(4)data.mdx @@ -151,7 +151,7 @@ type FormAction = (( ``` Form actions whose arguments are `[FormData]`, `[URLSearchParams]`, or `[]` implement Solid's serializable JSX attribute value. -Client actions need a stable name when rendered on the server. +Client form actions need a stable name when rendered on the server. Server functions supply their own URL. ### Form use @@ -162,15 +162,15 @@ 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 held update commits. +Only POST forms are accepted by delegated form action handling. +The form has `aria-busy="true"` from its delegated submission until the form 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. ### `with` Binds leading arguments and returns a form action for the remaining arguments. -The bound arguments are represented in the action URL. +The bound arguments are represented in the form action's URL. ### `onSubmit` @@ -181,7 +181,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 held update commits, after any revalidation or redirect it holds has rendered. +Callbacks run when the form action's held update commits, after any revalidation or redirect it holds has rendered. The submission record appears in `useSubmissions` at the same point. ## Server function responses