Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/routes/(3)building-apps/(3)server-functions/(0)index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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 `<form method="post" action={addToCartAction}>` 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.
Expand All @@ -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

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<form action={...}>` 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 `<form action={...}>` 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
Expand All @@ -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);

<form method="post" action={addToCartAction}>
<input type="hidden" name="productId" value={props.id} />
Expand Down Expand Up @@ -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
<form method="post" action={removeFromCart.with(line.productId)}>
Expand Down Expand Up @@ -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`

Expand All @@ -188,7 +188,7 @@ Solid Router reads the cookie on the next server render and records the submissi
## Recap

- Put every mutation behind `<form method="post" action={...}>` 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`.
Expand All @@ -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.
Loading
Loading