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
22 changes: 22 additions & 0 deletions scripts/extract-solid-ref.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ const CANONICAL_ROUTES = {
"Components & Context",
],
dynamic: ["web-components/dynamic.mdx", "Components"],
dynamicComponent: ["web-components/dynamic.mdx", "Components"],
lazy: ["components-context/lazy.mdx", "Components & Context"],
lazyModule: ["components-context/lazy.mdx", "Components & Context"],
useContext: ["components-context/use-context.mdx", "Components & Context"],

Errored: ["components-jsx/errored.mdx", "Components (JSX)"],
Expand Down Expand Up @@ -198,6 +200,10 @@ const CANONICAL_ROUTES = {
"request-response/configure-server-errors.mdx",
"Request & response",
],
reportRequestFailure: [
"request-response/configure-server-errors.mdx",
"Request & response",
],
redirect: ["request-response/redirect.mdx", "Request & response"],
reload: ["request-response/reload.mdx", "Request & response"],
respond: ["request-response/respond.mdx", "Request & response"],
Expand Down Expand Up @@ -261,6 +267,10 @@ const ADVANCED_ROUTES = {
"advanced/store-advanced/snapshot.mdx",
"Advanced / Store Advanced",
],
markRaw: [
"advanced/store-advanced/mark-raw.mdx",
"Advanced / Store Advanced",
],
storePath: [
"advanced/store-advanced/store-path.mdx",
"Advanced / Store Advanced",
Expand Down Expand Up @@ -291,6 +301,10 @@ const ADVANCED_ROUTES = {
"advanced/manual-hydration/no-hydration.mdx",
"Advanced / Manual Hydration",
],
onHydrationEnd: [
"advanced/manual-hydration/on-hydration-end.mdx",
"Advanced / Manual Hydration",
],

enableExternalSource: [
"advanced/interop-async/enable-external-source.mdx",
Expand All @@ -315,6 +329,10 @@ const ADVANCED_ROUTES = {
"advanced/diagnostics-dev-hooks/configure-client-errors.mdx",
"Advanced / Diagnostics & Dev Hooks",
],
dispatchAsInteraction: [
"advanced/diagnostics-dev-hooks/dispatch-as-interaction.mdx",
"Advanced / Diagnostics & Dev Hooks",
],
attribution: [
"advanced/diagnostics-dev-hooks/attribution.mdx",
"Advanced / Diagnostics & Dev Hooks",
Expand Down Expand Up @@ -667,6 +685,7 @@ const HIDDEN_EXPORTS = new Set([
"ssrHydrationKey",
"ssrRunInScope",
"ssrStyle",
"ssrStyleProperties",
"template",
// Compiler and DOM-runtime ABI. These exports support generated code and
// renderer integrations rather than application code.
Expand Down Expand Up @@ -2604,6 +2623,9 @@ const TYPE_TEXT_REWRITES = [
[/\bHydrationMemoOptions</g, "MemoOptions<"],
[/\bHydrationProjectionOptions\b/g, "ProjectionOptions"],
[/\bNoFn<([^<>]+)>/g, "$1"],
// The seedless derived form rejects array roots with an `& NoArray<T>`
// brand on its function; the prose says so, the signature need not.
[/\s*&\s*NoArray<[^<>]+>/g, ""],
[/\bRefreshable<((?:[^<>]|<[^<>]*>)+)>/g, "$1"],
// The core `Element` type is imported as `SolidElement` inside the
// package; app code knows it as `JSX.Element`.
Expand Down
4 changes: 3 additions & 1 deletion src/routes/(2)concepts/(1)components-and-jsx.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,9 @@ export function Preview() {
```

The source may also resolve to a native tag name or an async component.
See the [`dynamic()` reference](/reference/solid-web/components/dynamic).
Because a source may answer with a tag name, one `dynamic` on a page keeps the element runtime in the bundle.
When the source only ever answers with a component, as `Result` does, `dynamicComponent` from `solid-js` takes the same source and options without that cost; a tag name is a type error there.
See the [`dynamic()` reference](/reference/solid-web/components/dynamic), which covers both.

## Try it: remove a line from the cart

Expand Down
26 changes: 13 additions & 13 deletions src/routes/(2)concepts/(2)stores.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ Store writes are staged and applied in the same batch as signal writes, so a rea
[When updates land](/concepts/reactivity#when-updates-land) explains the batch and when to call `flush()`.

:::note[Path setters from Solid 1]
[`storePath`](/reference/solid-js/advanced/store-advanced/store-path) accepts the path-and-value form Solid 1 used, `setCart(storePath("coupon", "SAVE10"))`, for code that has not moved yet.
New code uses the draft.
The setter takes no path arguments.
A Solid 1 path setter such as `setCart("coupon", "SAVE10")` becomes a draft write to the same path, `setCart((draft) => { draft.coupon = "SAVE10"; })`.
Ranges and filters become ordinary loops over the draft, and setting a key to `undefined` to remove it becomes `delete`.
:::

## Derive a store with a projection
Expand All @@ -160,7 +161,8 @@ A projection is a store whose value is computed from other reactive values.
Where a memo derives one value, a projection derives an object or array whose properties are tracked separately, and whose items keep their identity from one computation to the next.

[`createProjection`](/reference/solid-js/stores/create-projection) takes a function and a seed.
The function receives a draft of the seed and may mutate it or return a replacement; a returned array is reconciled into the store by `id`:
The function receives a draft of the seed and may mutate it or return a replacement; a returned array is reconciled into the store by `id`.
The seed is a complete value of the store's type, and an array projection always takes one, `[]`:

```tsx
import { For, createProjection, createSignal, createStore } from "solid-js";
Expand Down Expand Up @@ -213,23 +215,23 @@ The projection function may return a promise.
The function form of `createStore` is a projection with a setter, and it is the normal way to load server data into a store:

```ts
const [cart, setCart] = createStore(async () => api.cart(), {
items: [] as CartItem[],
});
const [cart, setCart] = createStore(async () => api.cart());
```

The request starts when the store is created, and again whenever a reactive value the function read changes.
Each response reconciles into the same proxy by `id`, so items the server did not change keep their identity and their DOM.
A [`Loading`](/concepts/boundaries) boundary shows a fallback before the first response, [`isPending(() => cart.items)`](/reference/solid-js/reactivity/is-pending) reports a refetch, and [`refresh(cart)`](/reference/solid-js/lifecycle-actions/refresh) asks the server again.

With an async function, the seed is not shown as a first answer by default; readers wait for the first response the same way they wait for an async memo.
[Async reactivity](/concepts/async-reactivity) explains that wait, and the `seedLoadingValue` option that makes the seed an acceptable first answer.
This form takes no seed: the function reads no draft, and each response is a complete cart.
Readers wait for the first response the same way they wait for an async memo.
A store with an object root may omit the seed this way; an array root still takes `[]`, and a seed, when given, is a complete value of the store's type, not a partial one.
[Async reactivity](/concepts/async-reactivity) explains that wait, and the `seedLoadingValue` option that makes a seed an acceptable first answer; that option needs a seed.

Split the request into its own memo only when a second reader shapes the same response differently:

```ts
const response = createMemo(() => api.cart());
const [cart] = createStore(() => response(), { items: [] as CartItem[] });
const [cart] = createStore(() => response());
const itemCount = createMemo(() => response().items.length);
```

Expand All @@ -243,9 +245,7 @@ It shows immediately, and when the action settles Solid removes it and shows the
```ts
import { action, createOptimisticStore, refresh } from "solid-js";

const [cart, setCart] = createOptimisticStore(async () => api.cart(), {
items: [] as CartItem[],
});
const [cart, setCart] = createOptimisticStore(async () => api.cart());

const setQuantity = action(function* (id: string, quantity: number) {
setCart((draft) => {
Expand Down Expand Up @@ -293,7 +293,7 @@ Reassign the property to a new instance through the setter to notify readers, or
- Change state on the setter's draft; a write to the proxy itself is ignored.
- Assign the one property that changed rather than rebuilding the collection, so rows keep their identity.
- Derive a collection with `createProjection`, which reconciles results by `id`; derive a scalar with `createMemo`.
- Load server data with `createStore(async () => ..., seed)`; each response reconciles into the same proxy.
- Load server data with `createStore(async () => ...)`; each response reconciles into the same proxy, and an array root takes a `[]` seed.
- Make optimistic writes inside an `action` on a `createOptimisticStore`, and `refresh` the source when the request settles.

## Next steps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,22 @@ export async function createProduct(input: CreateProductInput) {
```

Call `createProduct(input)` from the admin page and the promise resolves to `product`, not to the envelope.
The transport applies the 201 and the header to the HTTP response, and a plain HTTP caller such as `curl` receives a JSON body with the same product in it.
`respond<T>()` is typed as `T`, so the function's inferred return type is the product's, and a direct call made on the server during SSR unwraps the envelope the same way.
The transport applies the 201 and the header to the HTTP response.
A plain HTTP caller such as `curl` receives a JSON body with the same product in it when JSON carries the value faithfully; a value JSON cannot represent, such as a `bigint`, is encoded the same way as a plain return.

:::deep-dive[The envelope on the wire]
`respond()` returns a `ResponseEnvelope`, an object holding the `Response` that carries the metadata and the `value` the caller should see.
At runtime `respond()` returns a `ResponseEnvelope`, a `Response` subclass that carries the status, headers, and body, plus the `value` the caller should see.
Because it is a real `Response`, a fetch-style handler with no Solid knowledge can answer with it as it is.
The server-function handler forwards the response's status and headers and encodes the value as the body.
Integration code that has to recognize an envelope, such as a router applying revalidation, uses [`isResponseEnvelope()`](/reference/solid-web/request-response/respond); the check is a registered symbol, so it works when a bundle contains two copies of the runtime.
Application code never touches the envelope directly.
:::

## Redirect the caller

`redirect()` returns a `Response` with a `Location` header:
`redirect()` returns a `Response` with a `Location` header.
Its return type defaults to `never`, so a returned redirect drops out of the function's inferred return type; `reload()` is typed the same way:

```ts
// src/data/account.ts
Expand Down
8 changes: 6 additions & 2 deletions src/routes/(3)building-apps/(6)middleware-and-api-routes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,21 @@ Production has no Vite pipeline, so every request that reaches the end of the ch
A middleware that wraps `next()` in `try`/`catch` sees anything the rest of the chain throws:

```ts
import { getRequestEvent, reportRequestFailure } from "@solidjs/web";

async function catchErrors(_request: Request, next: () => Promise<Response>) {
try {
return await next();
} catch (error) {
console.error(error);
reportRequestFailure(error, getRequestEvent()!);
return Response.json({ error: "Internal Server Error" }, { status: 500 });
}
}
```

Log the real error and return a generic body.
Report the real error and return a generic body.
`reportRequestFailure` hands the error to the server error hook as `kind: "request"`, the same hook that hears render and server-function failures, and logs it with `console.error` when no hook is registered; [Observability](/guides/observability#hear-every-failure-the-server-handles) sets that hook up.
An error the hook already heard, such as a failed render, is reported once.
Exception messages can carry database, filesystem, or token details; the only errors whose text belongs in a response are the ones the app marked safe, which [Mutations and responses](/building-apps/server-functions/mutations-and-responses#handle-thrown-errors) covers for server functions.
Production builds also wrap the generated page render in a default error boundary; set `start.errorBoundary: false` when this middleware owns errors, as [App structure](/building-apps/app-structure#common-problems) notes.

Expand Down
6 changes: 4 additions & 2 deletions src/routes/(4)routing/(1)solid-router/(0)index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const { paths } = Router;
Because the route tree is a literal, `paths.products()` without an argument is a type error, and so is `paths.produts`.

`lazy` splits each page into its own chunk.
The router loads a chunk when a link to it is hovered or when the route is matched, whichever comes first.
The router loads a chunk when the route is matched, or earlier when a [link preload strategy](/routing/solid-router/data#preloading-from-links) warms it.

## Mount it and add a layout

Expand Down Expand Up @@ -141,10 +141,11 @@ export const getProduct = query(async (id: string) => {
```tsx
// src/router.ts
import { lazy } from "solid-js";
import { createRouter } from "@solidjs/router";
import { createRouter, intentPreload } from "@solidjs/router";
import { getProduct } from "./data/products";

export const Router = createRouter({
preloadLinks: intentPreload(),
routes: [
{ path: "/", component: lazy(() => import("./pages/Home")) },
{
Expand Down Expand Up @@ -179,6 +180,7 @@ export default function Product(
}
```

`preloadLinks: intentPreload()` turns on link preloading, which is off by default.
Hover **Featured** and watch the network tab: the product request starts before the click.
Click, and the page renders with the data already there.

Expand Down
3 changes: 2 additions & 1 deletion src/routes/(4)routing/(1)solid-router/(1)setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ The other options exist for a specific situation each:
| `base` | The app is served under a prefix such as `/app`; matching and `paths` both include it. |
| `history` | The app is not a normal page: an Electron shell or `file://` needs `hashHistory()`, tests need `memoryHistory()`. |
| `preload` | The root layout needs data of its own; it runs once per mount or server request and reaches the function child as `props.data`. |
| `preloadLinks` | Hover and focus preloading costs more than it saves; set `false` and use `preload="false"` per link for finer control. |
| `preloadLinks` | Links should warm route code and data before the click; pass `intentPreload()` or another strategy (off by default). |
| `links` | Links should get `data-pending` during an in-flight navigation; pass `pendingLinks`. |
| `explicitLinks` | Some anchors inside the router must stay as full page loads; only anchors with a `link` attribute are then handled by the router. |
| `scrollRestoration` | The app manages scroll itself; set `false` to stop the router restoring the position on back and forward (on by default). |
| `transformUrl` | Incoming pathnames need rewriting before matching, for example to strip a locale prefix. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ const orderRoute = defineRoute({

`/account/orders/42` matches; `/account/orders/latest` falls through to the next route that matches, usually the `*404` catch-all.

Filters test the decoded parameter value, so `{ tag: ["café"] }` matches `/tags/caf%C3%A9`.

The built-in `int` filter accepts integer strings at runtime.
It also changes the corresponding `Router.paths` argument to `number`, so `paths.account.orders(42)` type-checks and `paths.account.orders("latest")` does not.

Expand Down Expand Up @@ -193,7 +195,7 @@ export default defineRoutes([
```

Visit `/` and the admin route table is not downloaded.
Visit `/admin/products/mug`, or hover a link to it, and the thunk runs, the router adds the resolved subtree to the compiled route tree, and the match completes.
Visit `/admin/products/mug`, or preload a link to it with a [link preload strategy](/routing/solid-router/data#preloading-from-links), and the thunk runs, the router adds the resolved subtree to the compiled route tree, and the match completes.
TypeScript infers `paths.admin.products("mug")` through the import's promise type when the imported route array retains literal types.

Use a deterministic thunk such as `() => import("./admin/routes")`.
Expand Down
Loading
Loading