diff --git a/src/routes/(3)building-apps/(3)server-functions/(0)index.mdx b/src/routes/(3)building-apps/(3)server-functions/(0)index.mdx index 4642265bf..39afde79b 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(0)index.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(0)index.mdx @@ -43,7 +43,7 @@ Search the client bundle for `database` and it is not there. The component did not change; `getProduct` returns a promise on both sides, so the [Async reactivity](/concepts/async-reactivity) rules apply to it unchanged. Server functions work without a router. -Solid Router's `query()` and `action()` add caching, submissions, and revalidation on top of them, and [Data loading and mutations](/routing/solid-router/data) covers that layer. +Solid Router's `query()` and `formAction()` add caching, submissions, and revalidation on top of them, and [Data loading and mutations](/routing/solid-router/data) covers that layer. ## Enable server functions @@ -197,9 +197,9 @@ The `X-Server-Function-Instance` header the client runtime sends identifies the ### The call resolves to a `Response` object instead of data -The function returned `redirect()` or `reload()`, and the caller was plain code rather than a router action. +The function returned `redirect()` or `reload()`, and no router was subscribed to the transport when it was called. The client transport hands responses that carry navigation or revalidation metadata back whole, so the integration that owns navigation can apply them. -Call the function through Solid Router's `action()`, or return a value and let the caller decide where to go; [Mutations and responses](/building-apps/server-functions/mutations-and-responses#redirect-the-caller) covers both. +Call the function with Solid Router mounted, which applies the metadata for a direct call or a form action alike, or return a value and let the caller decide where to go; [Mutations and responses](/building-apps/server-functions/mutations-and-responses#redirect-the-caller) covers both. ### The build fails with `server functions cannot capture non-top-level variables` @@ -239,4 +239,4 @@ Call the function from a render, a middleware, or another server function, or pr - [Reads, streams, and live data](/building-apps/server-functions/reads-and-live-data): declare a read that HTTP caches can store, and keep a stock level connected when another shopper changes it. - [Arguments and security](/building-apps/server-functions/arguments-and-security): what a caller can send, what the transport can encode, and how to validate and authorize every request. - [Mutations and responses](/building-apps/server-functions/mutations-and-responses): return a redirect, a reload, or a 400 from a cart mutation, and control what a thrown error reveals. -- [Data loading and mutations](/routing/solid-router/data): wrap the same functions in `query()` and `action()` for caching, submissions, and revalidation. +- [Data loading and mutations](/routing/solid-router/data): wrap the same functions in `query()` and `formAction()` for caching, submissions, and revalidation. 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 e107017d2..db76ff525 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 @@ -78,9 +78,9 @@ Return a redirect when it is the successful outcome, and throw it when it is an Both reach the caller the same way. What the browser does next depends on who made the call. -A Solid Router `action()` navigates to `/goodbye` without a page load. +With Solid Router mounted, a form action or a direct `deleteAccount()` call from an event handler navigates to `/goodbye` without a page load. An HTML form post without JavaScript is answered with the redirect itself, 302 by default, and the browser follows it. -Plain code that called `deleteAccount()` directly receives the `Response` object as the resolved value, because the transport hands navigation metadata back whole for the caller to apply. +Code that calls `deleteAccount()` with no router subscribed to the transport receives the `Response` object as the resolved value, because the transport hands navigation metadata back whole for the caller to apply. The target can be a string or an `Href` produced by a router's typed paths helper; a branded `Href` redirects to its logical path. Any other object throws `redirect() expects a string URL or an Href-branded value`. @@ -101,13 +101,32 @@ export async function addToCart(productId: string, quantity: number) { } ``` -Call `addToCart("mug", 1)` through a router action and the cart query refetches when the call settles; nothing else on the page is touched. +Call `addToCart("mug", 1)` with Solid Router mounted, from a form action or straight from an event handler, and the cart query refetches when the call settles; nothing else on the page is touched. Omit `revalidate` to ask for every cached read to refresh. Core carries the keys in an `X-Revalidate` header and does not define a cache. Solid Router reads the header against its query cache; [What revalidates after a mutation](/routing/solid-router/data#what-revalidates-after-a-mutation) explains the matching. `respond()` and `redirect()` accept the same `revalidate` option, for a mutation that also returns a value or changes location. +Solid Router treats every `POST` call as a mutation, and `POST` is the default. +A call that declares no `revalidate` keys revalidates every query on screen, the same default a form action has. +Reads are exempt when they go through the router's `query()`, which declares the function `GET`, or when the function is declared with `GET()`. +A `POST` that does not change anything, such as a log call or a search sent on each keystroke, declares an empty `revalidate` so it touches nothing: + +```ts +// src/data/telemetry.ts +import { respond } from "@solidjs/web"; + +export async function logEvent(name: string) { + "use server"; + + await analytics.record(name); + return respond(null, { revalidate: "" }); +} +``` + +`revalidate: ""` declares no keys, the same as `revalidate: []`; return `respond(value, { revalidate: "" })` when the call also returns a value. + ## Handle thrown errors A returned or thrown `Response` or envelope is control flow, and the handler preserves its status, headers, and value. @@ -168,17 +187,17 @@ return respond({ issues }, { status: 400 }); throw respond({ issues }, { status: 400 }); ``` -With the `Avoid` version the HTTP response is a 400, and the promise in the browser resolves as if the write had worked; a router action records it as a successful submission with `{ issues }` as its result. +With the `Avoid` version the HTTP response is a 400, and the promise in the browser resolves as if the write had worked; a router form action records it as a successful submission with `{ issues }` as its result. The status on a returned envelope is for HTTP callers; the throw is what tells the caller something failed. ::: ## Add router submissions -Solid Router's `action()` turns a server function into something a form can post to and the router can track: +Solid Router's `formAction()` turns a server function into something a form can post to and the router can track: ```ts // src/data/cart.ts -import { action } from "@solidjs/router"; +import { formAction } from "@solidjs/router"; import { reload } from "@solidjs/web"; export async function addToCart(form: FormData) { @@ -197,19 +216,19 @@ export async function addToCart(form: FormData) { return reload({ revalidate: "cart" }); } -export const addToCartAction = action(addToCart); +export const addToCartAction = formAction(addToCart); ``` Render `
` and the form posts to the function's URL before JavaScript loads; after hydration the router intercepts the submit, calls the function over the transport, and applies the `reload`. 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, `action(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 [Data loading and mutations](/routing/solid-router/data#mutate-with-actions) page uses that shape throughout, and the two behave the same. :::note[Which action] -`action` here is from `@solidjs/router`. -`solid-js` also exports an [`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` is from `@solidjs/router`; earlier releases called it `action`, and that name remains as a deprecated alias. +`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`. ::: When the router registers its single-flight integration, the mutation response can carry refreshed route data alongside the result; the server-function runtime treats that payload as opaque. @@ -229,8 +248,9 @@ A returned envelope resolves the caller with its value whatever the status; `thr ### The redirect comes back as a `Response` object -The function was called directly rather than through a router action, and the transport hands responses that carry a `Location` or `X-Revalidate` header back whole. -Call the function through `action()`, or return a value and navigate in the caller. +No router was subscribed to the transport when the function was called, so it handed the response that carries a `Location` or `X-Revalidate` header back whole. +That happens outside a mounted Solid Router, or with `singleFlight: false` set on it. +Call the function under a mounted router, or return a value and navigate in the caller. ### `redirect()` throws a `TypeError` about an Href-branded value @@ -240,12 +260,12 @@ Pass a string path, or the value a router's typed paths helper returns. ## Recap - Return a plain value when the caller needs data and nothing else; use `respond()` to add a status or headers to it. -- Return or throw `redirect(path)`; a router action navigates, a form post follows the redirect, and direct code receives the `Response`. -- Return `reload({ revalidate })` after a write so the router refetches the named reads. +- Return or throw `redirect(path)`; a mounted router navigates, a form post follows the redirect, and code with no router receives the `Response`. +- Return `reload({ revalidate })` after a write so the router refetches the named reads; a `POST` with no keys revalidates every query on screen, and `respond(value, { revalidate: "" })` opts out. - Throw `respond(value, { status })` for a failure the caller should see; a returned envelope resolves the caller whatever its status. - A plain thrown value becomes `Internal Server Error` in production; `markSafeError()` opts one `Error` out of that. - The development build is selected by the `development` export condition, not by `NODE_ENV`. -- Wrap a named server function with the router's `action()` for forms and submissions; write it inline only when nothing else calls it. +- Wrap a named server function with the router's `formAction()` for forms and submissions; write it inline only when nothing else calls it. ## Next steps 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 dcdebccbf..220ff4730 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 @@ -12,12 +12,12 @@ A server function has a URL, so the second version is available to every mutatio The server-function runtime recognizes a browser form post at that URL, decodes the body as one `FormData` argument, and answers in a way the browser can follow with no script. Application code reaches this through a router. -Solid Router's `action()` 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. +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. ## Post a form through a router action -Declare a server function that takes one `FormData` argument, and wrap it with `action` from `@solidjs/router`: +Declare a server function that takes one `FormData` argument, and wrap it with `formAction` from `@solidjs/router`: ```ts // src/data/cart.ts @@ -39,10 +39,10 @@ export async function addToCart(form: FormData) { ```tsx // src/pages/Product.tsx -import { action } from "@solidjs/router"; +import { formAction } from "@solidjs/router"; import { addToCart } from "../data/cart"; -const addToCartAction = action(addToCart); +const addToCartAction = formAction(addToCart); @@ -118,14 +118,14 @@ Whether TypeScript knows about `url` depends on how the function was declared: - A bare `"use server"` function keeps its declared function type. `url` is present at runtime and absent from the type, so `addToCart.url` is a type error. -For a form post, use a router action rather than the URL. +For a form post, use a router form action rather than the URL. An integration that must read the address from a bare declaration narrows it to `ServerFunction` from `@solidjs/web/server-functions` after checking `isServerFunction(fn)`. ## Bind leading arguments The function id lives in the URL path. An integration can add JSON-safe bound arguments in the reserved `args` query parameter, while the submitted form remains the final `FormData` argument. -Solid Router's `action.with(...)` does this for application code; with a `removeFromCart(productId: string, form: FormData)` action, the product id is bound and the form supplies the rest: +Solid Router's `formAction(...).with(...)` does this for application code; with a `removeFromCart(productId: string, form: FormData)` form action, the product id is bound and the form supplies the rest: ```tsx @@ -167,7 +167,7 @@ A host can configure `createNoJSHandler()` with a base path or supply another `h ### `Property 'url' does not exist` on a server function The function is a bare `"use server"` declaration, whose type is the function's own. -Pass it through the router's `action()` for a form, or, in integration code, narrow it to `ServerFunction` after `isServerFunction(fn)`. +Pass it through the router's `formAction()` for a form, or, in integration code, narrow it to `ServerFunction` after `isServerFunction(fn)`. ### `Bound arguments in an action url must be JSON-safe` @@ -188,7 +188,7 @@ Solid Router reads the cookie on the next server render and records the submissi ## Recap - Put every mutation behind `` so the browser can submit it before the client bundle runs. -- Wrap the server function with the router's `action()`; it serializes to the function's plain-HTTP URL and takes over after hydration. +- Wrap the server function with the router's `formAction()`; it serializes to the function's plain-HTTP URL and takes over after hydration. - Pending state, `useSubmissions`, and `.onSubmit` optimistic writes are layers the hydrated page adds; the HTML and the server function stay the same. - A browser form post follows a returned redirect; any other outcome is answered with a 303 back to the page and a flash cookie, which the router turns into a submission on the next render. - Use `method="get"` and `fn.url` only for a `GET()` read; the function receives the fields as `URLSearchParams`. @@ -199,5 +199,5 @@ Solid Router reads the cookie on the next server render and records the submissi ## Next steps - [Forms](/guides/forms): the checkout address form, working before hydration and adding inline messages and an optimistic address list after it. -- [Data loading and mutations](/routing/solid-router/data): what `action()` adds on top of this transport, including `.with()`, submissions, and revalidation. +- [Data loading and mutations](/routing/solid-router/data): what `formAction()` adds on top of this transport, including `.with()`, submissions, and revalidation. - [Sessions and auth](/building-apps/sessions-and-auth): the sign-in form, the most common no-JavaScript submission in a storefront. 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 81353df7f..bf3efeea1 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 @@ -119,25 +119,22 @@ The actions start and stop a job and return at once: ```ts // src/data/replies.ts -import { action } from "@solidjs/router"; +import { formAction } from "@solidjs/router"; import * as generations from "../server/generations"; import * as replies from "../server/replies"; import * as threads from "../server/threads"; -export const sendMessage = action( - async (threadId: string, messageId: string, text: string) => { - "use server"; - const { replyId, history } = await threads.append( - threadId, - messageId, - text - ); - generations.start(replyId, history); - }, - "sendMessage" -); +export async function sendMessage( + threadId: string, + messageId: string, + text: string +) { + "use server"; + const { replyId, history } = await threads.append(threadId, messageId, text); + generations.start(replyId, history); +} -export const stopReply = action(async (replyId: string) => { +export const stopReply = formAction(async (replyId: string) => { "use server"; generations.stop(replyId); }, "stopReply"); @@ -146,7 +143,8 @@ export const stopReply = action(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. -Both actions are Solid Router [actions](/routing/solid-router/data#mutate-with-actions), so they work as a form before JavaScript has loaded and from code through `useAction`. +`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. +`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] A server restart ends every running job and leaves the replies they were writing at `generating`. @@ -234,13 +232,11 @@ That is what [`until`](/reference/solid-js/lifecycle-actions/until) waits for: ```tsx // src/components/Composer.tsx import { For, action, createOptimisticStore, until } from "solid-js"; -import { useAction } from "@solidjs/router"; import { sendMessage } from "../data/replies"; type Pending = { id: string; text: string }; function Composer(props: { threadId: string; seen: string[] }) { - const send = useAction(sendMessage); const [outbox, setOutbox] = createOptimisticStore([]); const submit = action(function* (text: string) { @@ -248,7 +244,7 @@ function Composer(props: { threadId: string; seen: string[] }) { setOutbox((list) => { list.push({ id, text }); }); - yield send(props.threadId, id, text); + yield sendMessage(props.threadId, id, text); yield until(() => props.seen.includes(id), { timeout: 15_000 }); }); @@ -281,7 +277,7 @@ A timeout or a server error rejects the action, which drops the overlay the same The `For` also filters the outbox by `seen`. The action's overlay is released a task after the data that satisfied its `until` lands, so without the filter a frame can show the bubble and the row together; deriving the bubble's visibility from `seen` closes that gap, and `until` keeps its job of settling the action on failure and timeout. -`action` here is the one from `solid-js`, which runs a generator as one transaction; `sendMessage` is a Solid Router action, bound to the router with `useAction` so a call from code carries its revalidation and submission tracking. +`action` here is the one from `solid-js`, which runs a generator as one transaction; `sendMessage` is the server function itself, called directly, and the mounted router still applies its revalidation and any redirect. ## Server-owned: the thread as a live server component @@ -422,7 +418,7 @@ Await it from an action body or another imperative scope. ## Recap -- Start the generation from an action that returns at once; the job belongs to a module-level registry and writes the reply into a store as it arrives. +- Start the generation from a server function that returns at once; the job belongs to a module-level registry and writes the reply into a store as it arrives. - Pass the job its own abort signal; the request's signal ends only the watcher. - Deliver the reply through `live(GET(async function*))`, yielding the whole reply so far and the current reply first on every connection. - Render it in the browser with `createProjection`, or on the server as a live server component mounted with `dynamicComponent` through `liveQuery(live(GET(fn)), key)`. @@ -434,4 +430,4 @@ Await it from an action body or another imperative scope. - [Streaming AI responses](/building-apps/server-functions/streaming-ai-responses): the request-scoped shape, and the cancellation contract that decides between the two. - [Reads, streams, and live data](/building-apps/server-functions/reads-and-live-data): `live()`, its reconnect, and `onstatus`. - [Mutations](/concepts/mutations): actions, optimistic stores, and `until`. -- [Data loading and mutations](/routing/solid-router/data): Solid Router's `action`, `useAction`, and forms. +- [Data loading and mutations](/routing/solid-router/data): Solid Router's `formAction`, forms, and revalidation. diff --git a/src/routes/(4)routing/(0)overview.mdx b/src/routes/(4)routing/(0)overview.mdx index 0152f80ac..0c930682d 100644 --- a/src/routes/(4)routing/(0)overview.mdx +++ b/src/routes/(4)routing/(0)overview.mdx @@ -20,7 +20,7 @@ Nothing on this page is required reading for that. ## Pick a router Use Solid Router when you do not have a reason to use something else. -It is built for Solid's reactivity model: route params and location are reactive values, `query()` and `action()` are thin wrappers around server functions, forms submit before JavaScript loads, and single-flight mutations work by default. +It is built for Solid's reactivity model: route params and location are reactive values, `query()` and `formAction()` are thin wrappers around server functions, forms submit before JavaScript loads, and single-flight mutations work by default. It also ships the file-system adapter that the CLI templates use, so routes are files under `src/routes`. The `basic` and `fullstack` project shapes install it. 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 832c9ebb7..3461a1989 100644 --- a/src/routes/(4)routing/(1)solid-router/(0)index.mdx +++ b/src/routes/(4)routing/(1)solid-router/(0)index.mdx @@ -211,7 +211,7 @@ Everything else on these pages applies unchanged; the adapter only produces the [Convert a file-system manifest](/routing/solid-router/route-definitions#convert-a-file-system-manifest) shows where `preload` and the other route fields go in a route file. :::deep-dive[What the package entries are for] -`@solidjs/router` contains the router factory, route and navigation primitives, history adapters, queries, and actions, and is the only entry most applications import. +`@solidjs/router` contains the router factory, route and navigation primitives, history adapters, queries, and form actions, and is the only entry most applications import. `@solidjs/router/fs` converts a `file-routes` manifest into route definitions. `@solidjs/router/server` provides the single-flight data collector for a server-function handler, which the [Server rendering and hydration](/routing/solid-router/server-rendering) page installs. The [Solid Router API reference](/reference/solid-router) has signatures and option details for all three. @@ -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`, `action`, optimistic updates, and what revalidates after a mutation. +5. [Data loading and mutations](/routing/solid-router/data): `preload`, `query`, `formAction`, optimistic updates, and what revalidates after a mutation. 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/(5)data.mdx b/src/routes/(4)routing/(1)solid-router/(5)data.mdx index 41b7475f7..c0cf18c6e 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 actions, show optimistic state, and control what revalidates." +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." --- 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 `action` 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 `formAction` wraps a mutation so it works as a form without JavaScript and revalidates the cache when it finishes. This page continues the store: a product page and a cart. :::note[All of it is optional] @@ -192,16 +192,16 @@ revalidate(); // everything ``` Keys match by prefix, so the broad key reaches every argument combination. -Calling this by hand is rare; the usual trigger is an action finishing, covered below. +Calling this by hand is rare; the usual trigger is a mutation finishing, covered below. ## Mutate with actions -An `action` wraps a mutation so it can be submitted as a form, tracked while it runs, and followed by revalidation. +A `formAction` wraps a mutation so it can be submitted as a form, tracked while it runs, and followed by revalidation. Start with the server function: ```ts // src/data/cart.ts -import { query, action } from "@solidjs/router"; +import { query, formAction } from "@solidjs/router"; import { markSafeError, reload } from "@solidjs/web"; import { currentSessionId } from "./session"; @@ -210,7 +210,7 @@ export const getCart = query(async () => { return db.cart.forSession(currentSessionId()); }, "cart"); -export const addToCart = action(async (form: FormData) => { +export const addToCart = formAction(async (form: FormData) => { "use server"; const productId = String(form.get("productId")); const quantity = Number(form.get("quantity") ?? 1); @@ -239,13 +239,13 @@ Click **Add to cart** and the form gets `aria-busy="true"` until the call and it 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 action over the server-function transport, and applies the response. +With JavaScript, the router intercepts the submit, calls the form action over the server-function transport, and applies the response. Two rules follow from the form being the contract: -- Actions accept `POST` forms only; a `GET` form throws `Only POST forms are supported for Actions`. +- Form actions accept `POST` forms only; a `GET` form throws `Only POST forms are supported for Actions`. A `GET` form describes a URL, not a mutation; for search and filters, use a route with [typed search parameters](/routing/solid-router/navigation#type-search-parameters). -- Everything the action needs must be in the form or bound to the action. +- Everything the form action needs must be in the form or bound to it. Hidden inputs are the plain way; `.with()` is the typed way. The [Forms guide](/guides/forms) builds a complete form on this shape, including validation messages and no-JavaScript outcomes. @@ -255,7 +255,7 @@ The [Forms guide](/guides/forms) builds a complete form on this shape, including When the argument is known where the form is rendered, bind it instead of adding a hidden input: ```tsx -export const removeFromCart = action( +export const removeFromCart = formAction( async (productId: string, form: FormData) => { "use server"; await db.cart.remove(currentSessionId(), productId); @@ -272,22 +272,22 @@ export const removeFromCart = action( ; ``` -`.with(...)` returns an action whose remaining parameters start after the bound ones. +`.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 -An action does not have to call a server function. +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 = action(async (form: FormData) => { +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 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 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. ::: @@ -389,28 +389,40 @@ The optimistic layer is something the hydrated page adds on top of a form that a [Mutations](/concepts/mutations) explains the optimistic model in depth; this is the same model with the router providing the action. -## Invoke an action from code +## Call a mutation from code -When there is no form, for example a keyboard shortcut or a drag-and-drop handler, bind the action to the router and call it: +When there is no form, for example a keyboard shortcut or a drag-and-drop handler, call the server function directly: -```tsx -import { useAction } from "@solidjs/router"; - -const remove = useAction(removeFromCart); +```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 remove(selectedId(), new FormData()); + if (event.key === "Delete") void deleteCartLine(selectedId()); }} ``` -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. +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. +::: ## What revalidates after a mutation -When an action completes, the router revalidates the query cache. -By default that means every entry: the safest assumption after a write is that anything might have changed. +When a mutation completes, the router revalidates the query cache. +A mutation is a form action or any `POST` server function call, whether it came from a form, a core `action`, or an event handler; `POST` is the default method for server functions. +By default that means every query on screen: the safest assumption after a write is that anything might have changed. +Reads are exempt: `query` declares its server function as a `GET`, and so does wrapping one in `GET(fn)`. Narrow it from the server function's response: @@ -420,6 +432,12 @@ return respond(cart, { revalidate: [getCart.key, getTotals.key] }); // a value p return redirect(paths.cart, { revalidate: getCart.key }); // navigate, then revalidate ``` +A `POST` that does not change anything, such as a log call or a search sent on each keystroke, declares an empty `revalidate` so it touches nothing: + +```ts +return respond(results, { revalidate: "" }); // revalidate nothing +``` + A same-origin `redirect` becomes a router navigation; anything else leaves the app. The revalidation runs in the same update as the navigation, so the destination page renders with fresh data rather than flashing stale data first. @@ -434,39 +452,40 @@ In a `fullstack` project with the router's single-flight collector installed, th ### The list still shows the old data after a mutation -The mutation did not go through an `action`, so nothing revalidated: a server function called directly from an event handler updates the database and leaves the cache as it was. -Wrap it in `action` and submit it through a form or `useAction`. -If it is an action and one list is stale, the response narrowed `revalidate` to keys that do not include that list's query; add the key or return `reload()` without keys to revalidate everything. +The response narrowed `revalidate` to keys that do not include that list's query; add the key or return `reload()` without keys to revalidate everything. +If nothing revalidated at all, the mutation did not go through the router: a plain `fetch` to an API route, a call made where no router is mounted, or a server function called directly with `singleFlight: false` set on the router. +Call a server function, submit a form action, or call `revalidate` yourself after the write. ### `'use' router primitives can be only used inside a Route` when a query runs A `query` read inside a reactive scope binds to the router, so a component that reads one outside the ``, for example next to it in `App`, throws this. Move the reader inside the router's function child or a route component. -### The action ran but `useSubmissions` shows nothing +### The form action ran but `useSubmissions` shows nothing -The action returned nothing or redirected. +The form action returned nothing or redirected. Only a submission that produced a result or an error enters the list; register `.onSettled` to observe every completion, including void ones. ### The error message is `Internal Server Error` in production -The action threw a plain `Error`. +The mutation threw a plain `Error`. Wrap it in `markSafeError` to send the message to the browser, or throw the validation issues with `respond(issues, { status: 400 })`; a returned envelope is a successful result, not an error. ## Recap - 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 `action` and submit it with ``, so it works before JavaScript loads. +- 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. -- Narrow revalidation with `reload`, `respond`, or `redirect` options; without keys, every query revalidates. +- Call a server function directly when there is no form; the router still applies its revalidation and redirect. +- Narrow revalidation with `reload`, `respond`, or `redirect` options; without keys, every query revalidates after any `POST` server function, and `revalidate: ""` opts out. ## Next steps -- [Forms](/guides/forms): a complete form on router actions, from no-JavaScript through inline validation. +- [Forms](/guides/forms): a complete form on form actions, from no-JavaScript through inline validation. - [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`, `liveQuery`, `revalidate`, `action`, `useAction`, and `useSubmissions`. +- [Data API reference](/reference/solid-router/data): signatures for `query`, `liveQuery`, `revalidate`, `formAction`, and `useSubmissions`. 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 848f3c187..f6243e007 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 @@ -56,7 +56,7 @@ Adoption also works for reads that happen after hydration finishes, so a lazy ch ## One round trip for a mutation -Submit a form backed by an `action` on a page that also reads a `query`. +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. @@ -91,7 +91,7 @@ Submit **Add to cart** with both in place and the network tab shows one `POST /_ :::deep-dive[What the collector does on each mutation] When a mutation finishes, the collector works out which URL the client will show next, whether that is the current page or a `redirect` target. It resolves any lazy subtrees on that path, runs the root preload and the matched route preloads in data-only mode, and folds every `query` result they produced into the response. -On the client, the router applies the response's revalidation and redirect metadata, seeds the delivered values, and only then resolves the action call. +On the client, the router applies the response's revalidation and redirect metadata, seeds the delivered values, and only then resolves the mutation call. That is why the [Data](/routing/solid-router/data#what-revalidates-after-a-mutation) page recommends putting the reads a page needs in its `preload`: the collector can only refresh what the preloads touch. [Integrate a router](/routing/integrate-a-router#integrate-single-flight-mutations) describes the transport hooks the collector is built on. ::: @@ -156,6 +156,6 @@ With a custom server-function handler, pass `collectFlightData` to `handleServer ## Next steps - [Choose a rendering mode](/guides/choose-a-rendering-mode): static shell, streaming SSR, or prerendering, and what each asks of your route code. -- [Server functions](/building-apps/server-functions): the runtime under `query` and `action` in a `fullstack` project. +- [Server functions](/building-apps/server-functions): the runtime under `query` and `formAction` in a `fullstack` project. - [Deployment](/building-apps/deployment): where the server handler runs. - [`@solidjs/router/server` reference](/reference/solid-router/server): `createFlightDataCollector` options. diff --git a/src/routes/(4)routing/(3)integrate-a-router.mdx b/src/routes/(4)routing/(3)integrate-a-router.mdx index b2762034a..0a1c7f4df 100644 --- a/src/routes/(4)routing/(3)integrate-a-router.mdx +++ b/src/routes/(4)routing/(3)integrate-a-router.mdx @@ -108,8 +108,8 @@ Each named source receives only its own slice of the payload, so the two do not :::deep-dive[How the two supported routers use these hooks] Solid Router supplies `createFlightDataCollector` from `@solidjs/router/server`. It consumes the router's route tree, base, and root preload, then collects matched `query` results for the target URL. -On the client, it registers the mounted router when `singleFlight` is on, which is the default, and installs its flight consumer when the first router action is created. -The two sides use a rendezvous so either can load first: an action module in a lazily loaded route still attaches to the already mounted router, and a router-only app that never creates an action never subscribes, so the server is never asked to collect. +On the client, the router subscribes its flight consumer when it mounts, unless `singleFlight` is `false`; it is on by default. +Because the subscription belongs to the router rather than to a form action, every non-`GET` server-function call made while it is mounted carries the single-flight header, including a call made straight from an event handler. The consumer applies the response's redirect and revalidation metadata and seeds Solid Router's `query` cache from the payload. TanStack Router's integration registers a named source instead. diff --git a/src/routes/(5)guides/(4)forms.mdx b/src/routes/(5)guides/(4)forms.mdx index 5d38295d0..cb59cf1b7 100644 --- a/src/routes/(5)guides/(4)forms.mdx +++ b/src/routes/(5)guides/(4)forms.mdx @@ -1,7 +1,7 @@ --- title: "Forms" version: "2.0" -description: "Build a form with a server function and a Solid Router action: it works before JavaScript loads, validates on the server, and after hydration shows pending state, inline errors, and the saved result before the server confirms." +description: "Build a form with a server function and a Solid Router form action: it works before JavaScript loads, validates on the server, and after hydration shows pending state, inline errors, and the saved result before the server confirms." --- The checkout needs a shipping address. @@ -9,7 +9,7 @@ The form has to save the address when the user clicks **Continue**, reject a pos On a slow connection it has to do the first of those before the JavaScript has loaded. This guide builds that form in four passes. -The first is a form that posts to a server function through a Solid Router action, with no JavaScript in the browser at all. +The first is a form that posts to a server function through a Solid Router form action, with no JavaScript in the browser at all. The second adds validation the user cannot bypass. The third shows the validation messages inline and marks the form as pending, without changing the HTML. The fourth turns the form into an address book entry that appears in the list before the server has confirmed it. @@ -17,14 +17,14 @@ Each pass is a working form on its own; stop at the one that matches what the ap The guide assumes a `fullstack` project with [server functions](/building-apps/server-functions) enabled and [Solid Router](/routing/solid-router) mounted. -:::pitfall[Two functions named action] -This guide uses `action` from `@solidjs/router`. +:::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` also exports an [`action`](/reference/solid-js/lifecycle-actions/action); that one runs a generator as a reactive transaction and is covered in [Mutations](/concepts/mutations). -Editor auto-import offers both, and `` fails at the type level because the core `action` does not produce a URL. -The router's `action` uses the core one internally. -Every `action` in the code on this page is the router's. +`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. +The router's `formAction` uses the core one internally. ::: ## Pass 1: a form that posts to a server function @@ -49,14 +49,14 @@ export async function saveAddress(form: FormData) { } ``` -Wrap it in a router `action` and pass the action to the form: +Wrap it in a router `formAction` and pass the form action to the form: ```tsx // src/routes/checkout/address.tsx -import { action } from "@solidjs/router"; +import { formAction } from "@solidjs/router"; import { saveAddress } from "../../data/address"; -const submitAddress = action(saveAddress); +const submitAddress = formAction(saveAddress); export default function AddressPage() { return ( @@ -87,7 +87,7 @@ The server function is in `src/data/`, not next to the page. With method discovery on, a file under `src/routes` becomes an [API route](/building-apps/middleware-and-api-routes#api-routes) as soon as it exports an uppercase HTTP method; keeping server functions in `src/data` means a later export cannot turn one into a route by accident. ::: -`action(saveAddress)` returns a value that serializes to the server function's URL, so `action={submitAddress}` renders as an ordinary `action="..."` attribute in the HTML. +`formAction(saveAddress)` returns a value that serializes to the server function's URL, so `action={submitAddress}` renders as an ordinary `action="..."` attribute in the HTML. The browser posts to that URL, the server-function runtime sees that the request came from an HTML form rather than the Solid client, decodes the body as `FormData`, runs `saveAddress`, and follows the redirect. With JavaScript on, the router intercepts the submit instead. @@ -182,10 +182,10 @@ Read the recorded submissions with `useSubmissions` and show the messages next t ```tsx // src/routes/checkout/address.tsx import { Show } from "solid-js"; -import { action, useSubmissions } from "@solidjs/router"; +import { formAction, useSubmissions } from "@solidjs/router"; import { saveAddress, type AddressIssues } from "../../data/address"; -const submitAddress = action(saveAddress); +const submitAddress = formAction(saveAddress); export default function AddressPage() { const submissions = useSubmissions(submitAddress); @@ -274,7 +274,7 @@ The server side gets a cached read for the list and a second function that saves ```ts // src/data/address.ts -import { action, query } from "@solidjs/router"; +import { formAction, query } from "@solidjs/router"; import { reload } from "@solidjs/web"; export type SavedAddress = v.InferOutput & { @@ -308,14 +308,14 @@ The page reads the list through an optimistic store and pushes the submitted fie ```tsx // src/routes/checkout/address.tsx import { For, createOptimisticStore } from "solid-js"; -import { action } from "@solidjs/router"; +import { formAction } from "@solidjs/router"; import { addAddress, getAddresses, type SavedAddress, } from "../../data/address"; -const submitAddress = action(addAddress); +const submitAddress = formAction(addAddress); export default function AddressPage() { const [addresses, setAddresses] = createOptimisticStore( @@ -445,9 +445,9 @@ Check that the page is rendered inside the `Router`. ### `action` is not a function, or the form attribute renders as source code -The `action` came from `solid-js` instead of `@solidjs/router`. +The form was given the core `action` from `solid-js` instead of `formAction` from `@solidjs/router`. The core `action` wraps generator functions for reactive transactions and has no URL to serialize. -Import `action` from `@solidjs/router` for forms. +Import `formAction` from `@solidjs/router` for forms. ### Errors show for a moment and then vanish @@ -462,9 +462,9 @@ Use `throw respond(value, { status })` for structured failures. ## Recap -- Post a `FormData` argument to a server function through a router `action`, and the form works before JavaScript loads. +- Post a `FormData` argument to a server function through a router `formAction`, and the form works before JavaScript loads. - Keep server functions outside `src/routes`, where an uppercase method export would make the file an API route. -- Import `action` from `@solidjs/router` for forms; the core `action` has no URL. +- Import `formAction` from `@solidjs/router` for forms; the core `action` has no URL. - 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`. diff --git a/src/routes/(5)guides/(6)data-fetching-patterns.mdx b/src/routes/(5)guides/(6)data-fetching-patterns.mdx index 31c481ad6..5c9926bcc 100644 --- a/src/routes/(5)guides/(6)data-fetching-patterns.mdx +++ b/src/routes/(5)guides/(6)data-fetching-patterns.mdx @@ -249,7 +249,7 @@ Passing an accessor through context keeps the read lazy: nothing waits on the ca :::note[Sharing across routes] For requests shared across routes, or for deduplication and caching by argument, use [`query`](/routing/solid-router/data#cache-reads-with-query) from Solid Router. -It returns the same in-flight promise to every caller with the same arguments, keeps a result for a few minutes after the last reader leaves, and refetches when an action [revalidates](/routing/solid-router/data#revalidate) its key. +It returns the same in-flight promise to every caller with the same arguments, keeps a result for a few minutes after the last reader leaves, and refetches when a mutation [revalidates](/routing/solid-router/data#revalidate) its key. ::: ## Keep data fresh @@ -297,7 +297,7 @@ const addReview = action(function* (productId: string, text: string) { The reviews list does not flicker: the refetch runs inside the action, and the list updates once when the fresh data lands. To show the new review before the server confirms it, hold the list in a `createOptimisticStore` and write to it before the `yield`; [Mutations](/concepts/mutations) walks through that version. -With Solid Router, `action` from `@solidjs/router` adds submissions and automatic revalidation of `query` reads; the [Forms guide](/guides/forms) uses it. +With Solid Router mounted, a server function's response revalidates `query` reads automatically, and `formAction` from `@solidjs/router` adds form submissions on top; the [Forms guide](/guides/forms) uses it. ## Handle failures @@ -366,6 +366,6 @@ Read the input directly so both requests start together; with attribution enable - [Async reactivity](/concepts/async-reactivity): the model these patterns rest on, including held updates and optimistic writes. - [Server functions](/building-apps/server-functions): `GET` reads, `live` sources, and what a `"use server"` function does on the wire. -- [Data loading and mutations](/routing/solid-router/data): `query`, `preload`, and router actions. +- [Data loading and mutations](/routing/solid-router/data): `query`, `preload`, and form actions. - [Performance](/guides/performance#waterfalls): measuring a page whose first paint waits on sequential requests. - [Data fetching from Solid 1](/migration/data-fetching-from-solid-1): the same patterns from the other direction, for code that already exists. diff --git a/src/routes/(5)guides/(7)protected-routes.mdx b/src/routes/(5)guides/(7)protected-routes.mdx index 353b00898..c31c70b8a 100644 --- a/src/routes/(5)guides/(7)protected-routes.mdx +++ b/src/routes/(5)guides/(7)protected-routes.mdx @@ -131,7 +131,7 @@ export const requireUser = query(async (next: string) => { Open `/account/orders` signed out from a bookmark and the browser lands on `/sign-in?next=%2Faccount%2Forders`. 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. +Sign in, and the next visit to the account area runs the check again and finds the user, because a mutation that declares no `revalidate` keys, such as the sign-in form action, invalidates every cached query when it completes. 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. @@ -150,14 +150,14 @@ Any `X-Revalidate` keys on the redirect are invalidated before the navigation so ## Send them back -The sign-in page reads `next` from the URL and hands it to the sign-in action through a hidden field: +The sign-in page reads `next` from the URL and hands it to the sign-in form action through a hidden field: ```tsx // src/pages/SignIn.tsx -import { action, useSearchParams } from "@solidjs/router"; +import { formAction, useSearchParams } from "@solidjs/router"; import { safeNext, signIn } from "../data/account"; -const signInAction = action(signIn); +const signInAction = formAction(signIn); export default function SignIn() { const [search] = useSearchParams(); @@ -309,7 +309,7 @@ export function Header() { ``` Load the product page signed out and the header shows **Sign in** linking to `/sign-in?next=%2Fproducts%2Fmug`; the server rendered that link, and the browser adopted the serialized `current-user` result during hydration instead of fetching it again, so there is no request and no change of state on load. -Sign in and the name appears; sign out through a router `action` that throws `redirect(paths())` and every cached query is invalidated when the action completes, so `getCurrentUser` runs again and the header shows **Sign in** without a full page load. +Sign in and the name appears; sign out through a router `formAction`, or a sign-out server function called straight from a button, that throws `redirect(paths())` and every cached query is invalidated when the mutation completes, so `getCurrentUser` runs again and the header shows **Sign in** without a full page load. [State on the server](/guides/state-management#state-on-the-server) explains why the header holds a view of the user rather than a copy. `Header` renders inside the router's function child, so `useLocation()` and the `query` read both have a router to bind to. diff --git a/src/routes/(6)migration/(3)from-solid-router.mdx b/src/routes/(6)migration/(3)from-solid-router.mdx index 144abfe24..d9f5a26d8 100644 --- a/src/routes/(6)migration/(3)from-solid-router.mdx +++ b/src/routes/(6)migration/(3)from-solid-router.mdx @@ -442,12 +442,57 @@ When `query` wraps a server function that has no declared method, it obtains a G A plain function passed to `query` remains a plain function and does not load or use that transport. The router reads the function's [declaration metadata](/reference/solid-web/server-functions/metadata) when it needs to identify the transport method. +### Rename `action` to `formAction` + +The router's `action` is now `formAction`, and its `Action` type is `FormAction`. +The old names remain as deprecated aliases that log a development warning, and will be removed in a later major release. +Only the router's import changes; the `action` exported by `solid-js` is Solid's own primitive and keeps its name. + +Before: + +```tsx +import { action, type Action } from "@solidjs/router"; + +const addTodo: Action<[FormData], void> = action(saveTodo, "add-todo"); +``` + +After: + +```tsx +import { formAction, type FormAction } from "@solidjs/router"; + +const addTodo: FormAction<[FormData], void> = formAction(saveTodo, "add-todo"); +``` + +`useAction` is deprecated and has no replacement. +It still works, with a development warning. +Outside a form, call the server function directly, or wrap it in a core `action` from `solid-js` when the call also writes optimistic state. + +Before: + +```tsx +const remove = useAction(removeTodo); + +; +``` + +After: + +```tsx + +``` + +Here `deleteTodo` is the server function that `removeTodo` wrapped. +A mounted router applies the response of any server function call, form or not: its `revalidate` keys, `redirect()`, `reload()`, and single-flight data. +Unless `singleFlight: false` is set, every `POST` server function call counts as a mutation, and one that declares no `revalidate` keys revalidates every query on screen. +Reads through `query` and functions declared with `GET()` are exempt; a `POST` that changes nothing returns `respond(value, { revalidate: "" })` to touch nothing. + ### Replace pending submissions Router 0.x/1.x submissions represented pending and completed action calls. Router 2 separates those concerns: -- Use action `.onSubmit(...)` with Solid optimistic primitives for in-flight UI. +- Use form action `.onSubmit(...)` with Solid optimistic primitives for in-flight UI. - Use a form's `aria-busy="true"` state for form-level pending styling. - Use `useSubmissions()` for settled results and errors. - Use `.onSettled(...)` when every completion must be observed, including a void result or redirect. @@ -469,9 +514,9 @@ After: ```tsx import { For, createOptimisticStore } from "solid-js"; -import { action, useSubmissions } from "@solidjs/router"; +import { formAction, useSubmissions } from "@solidjs/router"; -const addTodo = action(async (form: FormData) => { +const addTodo = formAction(async (form: FormData) => { return saveTodo(String(form.get("title") ?? "")); }, "add-todo"); @@ -522,7 +567,7 @@ Registering it inside the component ties the hook to the component's lifetime. Router 2 retains a submission only when the action produces a result or error. A void or metadata-only completion still reaches `.onSettled(...)`. -Keep `method="post"` on action forms, and give a server-rendered client action a stable name. +Keep `method="post"` on form action forms, and give a server-rendered client form action a stable name. The rewritten [Data loading and mutations](/routing/solid-router/data#show-what-is-happening) page walks through this split with a running example. ### Move response helpers @@ -544,10 +589,10 @@ const save = action(async (form: FormData) => { After: ```tsx -import { action } from "@solidjs/router"; +import { formAction } from "@solidjs/router"; import { redirect, reload, respond } from "@solidjs/web"; -const save = action(async (form: FormData) => { +const save = formAction(async (form: FormData) => { const item = await saveItem(form); return respond(item, { revalidate: ["items"] }); }, "save-item"); @@ -740,6 +785,7 @@ See the [router factory reference](/reference/solid-router/router-factory) and [ - [ ] Remove `decodeURIComponent` calls on route parameters. - [ ] Replace `` and rename `useCurrentMatches`. - [ ] Replace `cache` with `query`. +- [ ] Rename the router's `action` and `Action` to `formAction` and `FormAction`, and replace `useAction` calls with direct server function calls. - [ ] Remove `createAsync`, `createAsyncStore`, and `useSubmission`. - [ ] Move pending UI to optimistic primitives or form `aria-busy`. - [ ] Import `redirect`, `reload`, and `respond` from `@solidjs/web`. @@ -748,4 +794,4 @@ See the [router factory reference](/reference/solid-router/router-factory) and [ - [ ] Register the single-flight collector before server-function dispatch when the application uses it. - [ ] Convert `` output through `fileRoutes(pageRoutes)`. - [ ] Replace widened route-array types with `defineRoutes`, `defineRoute`, and path witnesses. -- [ ] Remove the final imports of `Route`, `A`, `Navigate`, `createAsync`, `createAsyncStore`, `useSubmission`, `cache`, and router response helpers. +- [ ] Remove the final imports of `Route`, `A`, `Navigate`, `createAsync`, `createAsyncStore`, `useSubmission`, `useAction`, the router's `action`, `cache`, and router response helpers. diff --git a/src/routes/(7)glossary.mdx b/src/routes/(7)glossary.mdx index 09665c1c5..55f0ab870 100644 --- a/src/routes/(7)glossary.mdx +++ b/src/routes/(7)glossary.mdx @@ -108,7 +108,7 @@ 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 has its own `action`, which adds a form URL, submissions, and revalidation on top of a server function. +Solid Router's `formAction`, formerly also named `action`, 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). @@ -179,7 +179,7 @@ See [Revalidate](/routing/solid-router/data#revalidate) and [Request revalidatio ### Settled Async work has finished and its answer is the one the user can see. -Once a value has settled, a change to its input is held rather than blanking the screen; `onSettled` runs a callback once the first render has settled; a router action's `.onSettled(hook)` is a different API that runs after each submission completes. +Once a value has settled, a change to its input is held rather than blanking the screen; `onSettled` runs a callback once the first render has settled; a router form action's `.onSettled(hook)` is a different API that runs after each submission completes. See [Settled view and in-flight work](/concepts/async-reactivity#settled-view-and-in-flight-work). @@ -391,7 +391,7 @@ See [Derive a store with a projection](/concepts/stores#derive-a-store-with-a-pr ### redirect -Returns a `Response` with a `Location` header from a server function: a router action or query navigates without a page load, a form post without JavaScript follows the redirect, and plain code receives the `Response` object. +Returns a `Response` with a `Location` header from a server function: with Solid Router mounted, the call navigates without a page load, a form post without JavaScript follows the redirect, and code with no router receives the `Response` object. See [Redirect the caller](/building-apps/server-functions/mutations-and-responses#redirect-the-caller). @@ -506,7 +506,7 @@ See [Three rendering modes, one layout](/building-apps/app-structure#three-rende ### Submission -One run of a router action: the `input` that was sent, its `result` or `error`, and `retry()` and `clear()`. +One run of a router form action: the `input` that was sent, its `result` or `error`, and `retry()` and `clear()`. `useSubmissions` returns the completed submissions that produced a result or an error, including ones recorded from a form posted without JavaScript. See [When it fails](/routing/solid-router/data#when-it-fails). diff --git a/src/routes/reference/(3)solid-router/(0)index.mdx b/src/routes/reference/(3)solid-router/(0)index.mdx index 714056fa2..0a42bca5d 100644 --- a/src/routes/reference/(3)solid-router/(0)index.mdx +++ b/src/routes/reference/(3)solid-router/(0)index.mdx @@ -13,7 +13,7 @@ Solid Router is published through three package entries. ## Entries -- `@solidjs/router` exports the router factory, route helpers, navigation primitives, history adapters, queries, actions, and public types. +- `@solidjs/router` exports the router factory, route helpers, navigation primitives, history adapters, queries, form actions, and public types. - `@solidjs/router/fs` exports the file-system manifest adapter. - `@solidjs/router/server` exports the server-function flight-data collector. @@ -22,7 +22,7 @@ Solid Router is published through three package entries. - [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`, `liveQuery`, `revalidate`, `action`, `useAction`, and `useSubmissions` +- [Data APIs](/reference/solid-router/data): `query`, `liveQuery`, `revalidate`, `formAction`, and `useSubmissions`, plus the deprecated `action` and `useAction` - [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/(4)data.mdx b/src/routes/reference/(3)solid-router/(4)data.mdx index 2b7bada76..52f878657 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, live queries, revalidation, actions, action invocation, and settled submissions." +description: "Reference for Solid Router query caching, live queries, revalidation, form actions, and settled submissions." source_repo: "solidjs/solid-router" source_ref: "next" source_path: "src/data/index.ts" @@ -11,11 +11,10 @@ source_path: "src/data/index.ts" ```ts import { - action, + formAction, liveQuery, query, revalidate, - useAction, useSubmissions, } from "@solidjs/router"; ``` @@ -120,36 +119,38 @@ Keys match by prefix. Omit `key` to match all entries. `force` defaults to `true` and marks the matched entries stale before retriggering them. -## `action` +## `formAction` ```ts -function action( +function formAction( fn: (...args: T) => Promise, name?: string -): Action; +): FormAction; -function action( +function formAction( fn: (...args: T) => Promise, options?: { name?: string } -): Action; +): FormAction; ``` ```ts -type Action = (( +type FormAction = (( ...variables: T ) => Promise>) & { url: string; - with(...args: A): Action; - onSubmit(hook: (...args: V extends any[] ? V : T) => void): Action; + with(...args: A): FormAction; + onSubmit( + hook: (...args: V extends any[] ? V : T) => void + ): FormAction; onSettled( hook: ( submission: Submission> ) => void - ): Action; + ): FormAction; }; ``` -Form-compatible actions implement Solid's serializable JSX attribute value. +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. Server functions supply their own URL. @@ -168,7 +169,7 @@ The router does not overwrite or remove an `aria-busy` value the author set. ### `with` -Binds leading action arguments and returns an action for the remaining arguments. +Binds leading arguments and returns a form action for the remaining arguments. The bound arguments are represented in the action URL. ### `onSubmit` @@ -183,27 +184,51 @@ The callback receives a `Submission`, including for void, redirect, and metadata 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` +## Server function responses + +While a router is mounted and `singleFlight` is not `false`, the router applies the response of every non-`GET` server-function call, whether or not a form action made it. +It applies `X-Revalidate` keys, `redirect()`, `reload()`, and single-flight data. +A `POST` call that declares no `revalidate` keys revalidates every query. +Functions wrapped by `query` or declared with `GET()` are reads and are exempt. +`revalidate: ""` declares no keys, the same as `revalidate: []`. + +## Deprecated + +### `action` + +```ts +const action: typeof formAction; +type Action = FormAction; +``` + +Deprecated alias of `formAction`, and of the `FormAction` type. +Logs a development warning on first use. +Unrelated to the `action` exported by `solid-js`. + +### `useAction` ```ts function useAction( - action: Action + action: FormAction ): (...args: T) => Promise>; ``` -Binds direct action invocation to the current router context. +Deprecated, with no replacement. +Logs a development warning. +Call the server function directly, or a core `action` from `solid-js` around it. +Binds direct form 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` ```ts function useSubmissions( - action: Action, + action: FormAction, filter?: (input: V) => boolean ): Submission>[]; ``` -Returns a reactive array proxy of settled records for the action. +Returns a reactive array proxy of settled records for the form action. The router retains only records with a result or error. ```ts diff --git a/src/routes/reference/(3)solid-router/(8)types.mdx b/src/routes/reference/(3)solid-router/(8)types.mdx index 3c922239d..1a89c033b 100644 --- a/src/routes/reference/(3)solid-router/(8)types.mdx +++ b/src/routes/reference/(3)solid-router/(8)types.mdx @@ -133,8 +133,9 @@ See [History adapters](/reference/solid-router/history) for the supported built- - `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. +- `FormAction` is returned by `formAction`. +- `Action` is a deprecated alias of `FormAction`. +- `Submission` is a settled form action record. See [Data APIs](/reference/solid-router/data) for signatures.