diff --git a/src/routes/(1)getting-started/(1)project-shapes.mdx b/src/routes/(1)getting-started/(1)project-shapes.mdx index eee1ea743..5861d4250 100644 --- a/src/routes/(1)getting-started/(1)project-shapes.mdx +++ b/src/routes/(1)getting-started/(1)project-shapes.mdx @@ -5,8 +5,8 @@ description: "Pick between the bare, basic, and fullstack templates by what your --- The CLI asks one question before it writes any files: `bare`, `basic`, or `fullstack`. -The answer decides what is installed and what the build produces, and it looks like a commitment. -It is not: each shape is a strict superset of the one before, and the `src/App.tsx` and `src/Document.tsx` conventions are the same in all three, so an app that starts as `bare` becomes `fullstack` by changing configuration rather than rewriting components. +The answer decides what is installed and what the build produces, but you can change it later. +Each shape is a strict superset of the one before, and the `src/App.tsx` and `src/Document.tsx` conventions are the same in all three, so an app that starts as `bare` becomes `fullstack` by changing configuration rather than rewriting components. Pick by what the app needs on its first deploy: diff --git a/src/routes/(2)concepts/(0)reactivity.mdx b/src/routes/(2)concepts/(0)reactivity.mdx index 846293b10..7a5188a87 100644 --- a/src/routes/(2)concepts/(0)reactivity.mdx +++ b/src/routes/(2)concepts/(0)reactivity.mdx @@ -8,7 +8,7 @@ In the [Quick start](/getting-started/quick-start) you saw two versions of the s One read `count()` inside the JSX and updated on every click. The other read `count()` in the component body, and the button froze at zero while the console warned that the read "will not update". -Everything on this page follows from the difference between those two lines. +This page explains why. Solid does not re-run components. It runs each component once, records which reactive values each expression reads, and re-runs only those expressions when the values they read change. The place where you read a value decides whether Solid can see the read. diff --git a/src/routes/(2)concepts/(1)components-and-jsx.mdx b/src/routes/(2)concepts/(1)components-and-jsx.mdx index 90980fb43..c6861a84d 100644 --- a/src/routes/(2)concepts/(1)components-and-jsx.mdx +++ b/src/routes/(2)concepts/(1)components-and-jsx.mdx @@ -6,11 +6,9 @@ description: "Components as functions that run once: props, children, control fl A Solid component is a function that runs once. It sets up state, returns JSX, and is not called again for the life of the component. -If you come from a framework where components re-render, this is the fact to hold on to while reading this page: every rule about props, children, and control flow below exists because the function body will not run a second time. - -The [Reactivity](/concepts/reactivity) page explains how the JSX inside a component keeps updating after the function has returned. -This page covers the rest of what a component does: receiving props, handling events, reaching the DOM, rendering lists and conditions, and composing with other components. -The examples continue the shopping cart from that page. +Every rule below about props, children and control flow follows from that: the function body will not run a second time. +[Reactivity](/concepts/reactivity) explains how the JSX keeps updating after the function has returned. +This page covers props, events, refs, lists and conditions, and composition, continuing the shopping cart from that page. ## How JSX executes diff --git a/src/routes/(2)concepts/(2)stores.mdx b/src/routes/(2)concepts/(2)stores.mdx index e5f237c61..d96146234 100644 --- a/src/routes/(2)concepts/(2)stores.mdx +++ b/src/routes/(2)concepts/(2)stores.mdx @@ -4,13 +4,10 @@ version: "2.0" description: "Hold a cart, a form, or any nested object in a store: update one property through a draft, derive filtered views with a projection, and load server data into the same proxy." --- -The cart on the [Components and JSX](/concepts/components-and-jsx) page held its items in a signal. -That works until the user edits a quantity. -To change one number you must build a new array with one new object in it, and `For` sees a new object where the old row was: the row is torn down and rebuilt, and if the user was typing in that row's input, the input they were typing in is gone. - -A store fixes this by tracking each property on its own. -Change `items[1].quantity` and the one text node that reads it updates. -Nothing is copied, no row is rebuilt, and the input keeps focus. +The cart on the [Components and JSX](/concepts/components-and-jsx) page keeps its items in a signal, so changing one quantity means building a new array with a new object in it. +`For` sees a new object where the old row was and rebuilds the row, and the input the user was typing in is replaced. +A store tracks each property on its own instead. +Change `items[1].quantity` and the one text node that reads it updates, and the row and its input stay put. Use a signal when a value is read and replaced as one unit: a count, a selected id, a string. Use a store when readers need separate parts of an object or array, which describes most application state. diff --git a/src/routes/(2)concepts/(3)async-reactivity.mdx b/src/routes/(2)concepts/(3)async-reactivity.mdx index 91564a91f..52941439f 100644 --- a/src/routes/(2)concepts/(3)async-reactivity.mdx +++ b/src/routes/(2)concepts/(3)async-reactivity.mdx @@ -4,12 +4,8 @@ version: "2.0" description: "Read a promise like a value: how async memos hold updates, when Loading shows a fallback, and how mutations settle." --- -Take one value your UI renders today: a product, a list, a setting. -Make it come from the server instead of a constant. -Written by hand, that change spreads: the type becomes `Promise`, every component between the fetch and the display learns about loading, and the place you fetch and the place you show a spinner become the same place whether you want that or not. - -In Solid the change is one line. -A computation that returns a promise is still a computation, and everything that reads it keeps reading a value. +Take a value your UI renders today, such as a product, and load it from the server instead of a constant. +In Solid that is a one-line change: a computation that returns a promise is still a computation, so the type does not become `Promise`, and the components between the fetch and the display do not need to know about loading. That one line also changes how writes behave. Once a memo is async, a write to any input it depends on is held until the memo has its next answer, and everything else in the same update waits with it. diff --git a/src/routes/(2)concepts/(4)boundaries.mdx b/src/routes/(2)concepts/(4)boundaries.mdx index 6f683ab51..8a817155d 100644 --- a/src/routes/(2)concepts/(4)boundaries.mdx +++ b/src/routes/(2)concepts/(4)boundaries.mdx @@ -4,10 +4,8 @@ version: "2.0" description: "Decide which part of the page shows a spinner, which part shows an error, and in what order regions appear, by placing Loading, Errored, and Reveal around the right subtree." --- -A product page loads the product, its reviews, and a list of recommendations. -With one boundary around the whole page, the first request to fail takes everything down with it, and a slow recommendations query keeps the product itself behind a spinner. +With one boundary around a whole product page, a slow recommendations query keeps the product behind a spinner, and one failed request takes the whole page down. With no boundary at all, an unhandled error halts the reactive system and nothing on the page updates again. - Boundaries decide how much of the page a pending or failed read affects. [`Loading`](/reference/solid-js/components-jsx/loading) renders a fallback while async reads in its subtree have no value, [`Errored`](/reference/solid-js/components-jsx/errored) renders a fallback when its subtree throws, and [`Reveal`](/reference/solid-js/components-jsx/reveal) decides the order in which sibling regions appear. Where you put each one is the design decision this page is about. diff --git a/src/routes/(2)concepts/(5)mutations.mdx b/src/routes/(2)concepts/(5)mutations.mdx index 43438136a..fb48b0cd0 100644 --- a/src/routes/(2)concepts/(5)mutations.mdx +++ b/src/routes/(2)concepts/(5)mutations.mdx @@ -4,13 +4,10 @@ version: "2.0" description: "Move the cart's add, remove, and set-quantity writes to a server without changing the UI: actions, optimistic stores, refresh, and how a failed request undoes itself." --- -The user clicks **Add to cart**. -The request takes 400 milliseconds. -In that time the cart must show the new line, and when the response arrives the cart must agree with the server; if the server refuses, the line must go away again. - -Reads need no wrapper for any of this: change an input and the memo asks again, as the [Async reactivity](/concepts/async-reactivity) page showed. -Writes have a different shape, because a write happens before the round trip and must be reconciled after it. -This page takes the cart from the [Stores](/concepts/stores) page and moves its writes to a server one change at a time. +Reads do not need any wrapper to talk to a server: change an input and the memo asks again, as [Async reactivity](/concepts/async-reactivity) showed. +Writes are harder, because the write happens before the round trip and has to be reconciled after it. +When the user clicks **Add to cart**, the cart should show the new line right away, agree with the server when the response arrives, and drop the line if the server refuses. +This page takes the cart from [Stores](/concepts/stores) and moves its writes to a server one step at a time. ## A client-only cart diff --git a/src/routes/(2)concepts/(6)rendering-and-ssr.mdx b/src/routes/(2)concepts/(6)rendering-and-ssr.mdx index 2b5c0fa53..bf4678f09 100644 --- a/src/routes/(2)concepts/(6)rendering-and-ssr.mdx +++ b/src/routes/(2)concepts/(6)rendering-and-ssr.mdx @@ -11,7 +11,7 @@ ReferenceError: window is not defined ``` The stack points at a module that read `localStorage` when it was imported. -Nothing about the component was wrong in the browser; it is now also running in Node, where there is no `window`, and the same source has to be correct in both places. +The component was fine in the browser, but it now also runs in Node, where there is no `window`. Solid uses one component source for client rendering and server-side rendering (SSR). The JSX build target selects DOM operations for the browser or HTML-producing operations for the server. diff --git a/src/routes/(3)building-apps/(0)app-structure.mdx b/src/routes/(3)building-apps/(0)app-structure.mdx index ae644812f..b4798e1f2 100644 --- a/src/routes/(3)building-apps/(0)app-structure.mdx +++ b/src/routes/(3)building-apps/(0)app-structure.mdx @@ -7,7 +7,6 @@ description: "Know which two files in a start-mode project you edit, what the pl Open the project the [Quick start](/getting-started/quick-start) created and two things are missing that every Vite template has: there is no `index.html`, and no file calls `render()` or `hydrate()`. What is there is `src/App.tsx`, `src/Document.tsx`, and a `vite.config.ts` with `start: true`. -Those two components are the whole application surface. `App` is the component every page renders inside; `Document` is the HTML shell around it. The plugin's start mode generates the rest: the server entry that renders ``, the client entry that mounts or hydrates it, and the request handler that serves both. diff --git a/src/routes/(3)building-apps/(1)styling-and-assets.mdx b/src/routes/(3)building-apps/(1)styling-and-assets.mdx index 901ae3bdd..b7d255d94 100644 --- a/src/routes/(3)building-apps/(1)styling-and-assets.mdx +++ b/src/routes/(3)building-apps/(1)styling-and-assets.mdx @@ -4,11 +4,7 @@ version: "2.0" description: "Style a component with global CSS, a CSS module, Tailwind, or an inline style object, toggle classes without string building, and reference images so the built URL is hashed." --- -The product card needs a border and the header needs the store logo. -In a Vite single-page app the reflex is a `` in `index.html` and an ``. -A start-mode project has no `index.html`, and `src/` is not served in production, so neither reflex has anywhere to go. - -Everything reaches the page through imports instead. +A start-mode project has no `index.html`, and `src/` is not served in production, so stylesheets and images reach the page through imports instead. Import a stylesheet and Vite includes it in the build; import an image and the import is its final, hashed URL. Solid adds nothing to that pipeline; what it adds is the `class` and `style` props that apply the result. diff --git a/src/routes/(3)building-apps/(2)head-and-metadata.mdx b/src/routes/(3)building-apps/(2)head-and-metadata.mdx index 67f9e882c..483503e9a 100644 --- a/src/routes/(3)building-apps/(2)head-and-metadata.mdx +++ b/src/routes/(3)building-apps/(2)head-and-metadata.mdx @@ -4,10 +4,7 @@ version: "2.0" description: "Give each page its own title and meta tags from the component that owns them, let a layout's defaults return when the page unmounts, and read a title from data that is still loading." --- -Every product page in the store has the same browser tab title, "Solid App", because the only `` is the one in `Document.tsx`. -Paste a product link into a chat and the preview shows the same default image for every product. -The component that knows the product name renders inside `<body>`; the tags that need it live in `<head>`, where a component cannot render. - +A page title or a link-preview image usually depends on data that only a component deep in `<body>` knows, like the product name, but those tags live in `<head>`, where a component cannot render. Solid ships a head registry in `@solidjs/web` for this. A component declares a title or a meta tag from wherever it lives, and the registry puts the tag in the document head, replaces it when a later component declares the same tag, and restores the earlier one when that component unmounts. `@solidjs/meta` wraps the registry in components. 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 39afde79b..09882e938 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 @@ -4,11 +4,8 @@ version: "2.0" description: 'Move a database query or a secret out of the browser by marking a function with "use server", and recognize what that call becomes on the wire.' --- -The product page needs the product record. -The record lives in a database, the database client needs a connection string, and neither the client nor the string can ship to the browser. -The usual answer is a separate HTTP API: a route module, a fetch call, a type for the response, and two places to keep in step. - -A server function is the same call without the second place. +The product page needs a record from a database, and neither the database client nor its connection string can ship to the browser. +A server function lets the component call that code directly, without a separate HTTP API to write and keep in step. Add `"use server"` to the function, keep calling it from the component, and the build moves the body to the server and leaves a typed stub in the browser that makes the HTTP request for you. ```ts diff --git a/src/routes/(3)building-apps/(3)server-functions/(1)reads-and-live-data.mdx b/src/routes/(3)building-apps/(3)server-functions/(1)reads-and-live-data.mdx index 18a88c6b2..798f731df 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(1)reads-and-live-data.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(1)reads-and-live-data.mdx @@ -4,13 +4,10 @@ version: "2.0" description: "Declare a product read that HTTP caches can store, stream order events from a server function, and keep a stock level connected when another shopper changes it." --- -The product page from the [Server functions](/building-apps/server-functions) page shows "3 left in stock". -Another shopper buys one, and the number on this page is wrong until someone reloads. -The same page fetches the product record on every visit, and because a server function call is a `POST` by default, no cache between the browser and the server is allowed to remember the answer. - -Both problems are about how a read reaches the server. +A server function call is a `POST` by default, so no cache between the browser and the server can remember the answer, and the value on screen only changes when something asks again. +This page covers the two ways to change how a read reaches the server. `GET()` declares a read the transport may send as a cacheable `GET` request. -`live()` declares a read that produces values over time and reconnects when the connection drops, so the stock level follows the other shopper's purchase. +`live()` declares a read that produces values over time and reconnects when the connection drops, so a stock count updates when someone else buys the last one. Most applications need the [GET read](#declare-a-get-read) section, and only when a read is called outside Solid Router's `query()`, plus [live sources](#declare-a-live-source) for a value that changes under the user. Streams and connection status are for the parts of an app that consume events directly. diff --git a/src/routes/(3)building-apps/(3)server-functions/(2)arguments-and-security.mdx b/src/routes/(3)building-apps/(3)server-functions/(2)arguments-and-security.mdx index a64962557..db03aafd0 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(2)arguments-and-security.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(2)arguments-and-security.mdx @@ -4,10 +4,8 @@ version: "2.0" description: "Send arguments a server function can decode, and validate and authorize every call as if it came from a terminal, because it can." --- -The account page lets a shopper change the name on their account. -The component calls `updateAccountName(name)`, TypeScript checks that `name` is a string, and the function writes it to the database. - -Every server function is an HTTP endpoint, and the component is one caller among many: +Every server function is an HTTP endpoint, and your component is only one of its callers. +Say the account page calls `updateAccountName(name)` and TypeScript checks that `name` is a string. Anyone can still send this: ```bash curl -X POST 'https://shop.example/_server/<id>?args=%5B%7B%22admin%22%3Atrue%7D%5D' \ 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 fd67ae414..68d35c037 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 @@ -4,13 +4,8 @@ version: "2.0" description: "Return a value, a redirect, a reload, or a 400 from a cart or account mutation, and control what a thrown error reveals in production." --- -The Add to cart form on the product page posts to `addToCart`. -When the write succeeds, the browser should land on the cart with the new line in it. -When the quantity is not a whole number, the caller should get a 400 and a message it can show next to the field. -When the database is down, the caller should get an error, and that error must not contain the failing query. - -A return value can carry the cart line. -It cannot carry a status, a `Location`, or the instruction to refresh the cart, so those go through the response helpers in `@solidjs/web`. +A server function that writes usually needs to say more than its return value can: redirect to the cart after adding to it, answer a bad quantity with a 400 and a message, or fail without leaking the database query. +A status, a `Location` header, or an instruction to refresh the cart goes through the response helpers in `@solidjs/web`. Most applications need [`redirect()`](#redirect-the-caller), [`reload()`](#request-revalidation), and `throw respond(...)` from [Handle thrown errors](#handle-thrown-errors); the envelope internals are for integrators. Mutations stay on the default `POST` transport, which is origin-checked. diff --git a/src/routes/(3)building-apps/(3)server-functions/(4)metadata-and-transport.mdx b/src/routes/(3)building-apps/(3)server-functions/(4)metadata-and-transport.mdx index c59a0981f..95aee1b0c 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(4)metadata-and-transport.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(4)metadata-and-transport.mdx @@ -6,7 +6,7 @@ description: "Read a server-function request in the network tab, attach a header Open the network tab and click Add to cart. The request is a `POST` to `/_server/data/<id>`, it carries `X-Server-Function-Instance` and `X-Server-Function-Format` headers the client runtime added, its body is the argument array, and the response leaves with `Cache-Control: no-store`. -Nothing on the product page asked for any of that; the transport has defaults, and most applications never change them. +Those are the transport's defaults, and most applications never change them. This page is for the three cases where the defaults are not enough: every request needs a header that changes during the session, such as a bearer token; the endpoint has to live somewhere other than `/_server`; or one call needs to be cancelled while it is in flight. Most applications need [Prepare every client request](#prepare-every-client-request) and [Configure one call](#configure-one-call). 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 a76598c6f..1967d693f 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 @@ -4,11 +4,9 @@ version: "2.0" description: "Make the Add to cart form post to a server function before JavaScript loads, and know which part of that exchange core owns and which the router adds." --- -A shopper on a slow connection opens the product page and clicks Add to cart before the client bundle has arrived. -If the button's only wiring is an `onClick` handler, nothing happens, and the click is lost. -If the button is inside a form whose `action` is a URL, the browser posts the form, the server runs the function, and the shopper lands on the cart. - -A server function has a URL, so the second version is available to every mutation on this cluster's pages. +A form whose `action` is a URL works before your JavaScript loads: the browser posts it, the server runs the function, and the user lands on the next page. +An `onClick` handler does nothing until the bundle arrives. +Every server function has a URL, so any mutation can work the first way. 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. diff --git a/src/routes/(3)building-apps/(3)server-functions/(6)streaming-ai-responses.mdx b/src/routes/(3)building-apps/(3)server-functions/(6)streaming-ai-responses.mdx index be97180e3..864f0b8f9 100644 --- a/src/routes/(3)building-apps/(3)server-functions/(6)streaming-ai-responses.mdx +++ b/src/routes/(3)building-apps/(3)server-functions/(6)streaming-ai-responses.mdx @@ -4,12 +4,9 @@ version: "2.0" description: "Forward a model's token stream through a server function into a store one chunk at a time, stop it from the browser or by asking a new question, and render the Markdown without re-rendering what has already landed." --- -The storefront gets a support assistant: a shopper types a question and the answer appears word by word as the model produces it. -Halfway through, the shopper asks a different question, or presses Stop, or the model call fails and they want to try again. -Written by hand, that is a `fetch` with a body reader, a flag for "streaming", an `AbortController` kept somewhere, and a text state that is replaced on every chunk. - -In Solid the model call is a server function that returns an async generator, the browser side is a projection that writes one store commit per chunk, and cancellation is what the reactive graph does with a computation it no longer needs. -This guide builds that assistant and ends with the cancellation contract, because deciding who may stop a generation is the part that does not fall out of the code. +This guide builds a support assistant whose answer appears word by word, and that the shopper can stop, retry, or interrupt with a new question. +The model call is a server function that returns an async generator, the browser side is a projection that writes one store update per chunk, and cancelling is what the reactive graph already does with a computation it no longer needs. +The guide ends with who is allowed to stop a generation, because that is the one part you have to decide yourself. ## Stream from the server 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 912a5d3f5..7f2a0006b 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 @@ -4,10 +4,8 @@ version: "2.0" description: "Run an AI reply as a job that outlives the request that started it, store the reply as it grows, and deliver it through a live source so a closed tab loses nothing and two tabs read the same reply." --- -The support assistant from [Streaming AI responses](/building-apps/server-functions/streaming-ai-responses) answers while the shopper watches, and stops when they leave. -That is the right contract for a quick question. -It is the wrong one for a summary of a forty-message order dispute: the shopper closes the tab to find the order number, comes back, and the summary starts over, because the request that was writing it ended with the tab. - +The assistant from [Streaming AI responses](/building-apps/server-functions/streaming-ai-responses) stops when the shopper leaves, which is right for a quick question. +For a long summary that should finish even if the tab closes, and still be there on reload, the generation has to outlive the request. The difference is who owns the generation. In the streaming guide the request owns it, and the reactive graph cancels it. On this page a job on the server owns it: a server function starts the job and returns, the job writes the reply into storage as the model produces it, and every page that shows the reply reads what is stored through a live source. diff --git a/src/routes/(3)building-apps/(4)sessions-and-auth.mdx b/src/routes/(3)building-apps/(4)sessions-and-auth.mdx index a21fa099d..d28cc30ad 100644 --- a/src/routes/(3)building-apps/(4)sessions-and-auth.mdx +++ b/src/routes/(3)building-apps/(4)sessions-and-auth.mdx @@ -4,9 +4,7 @@ version: "2.0" description: "Build a signed cookie session on the request event, sign customers in and out from server functions, and authorize every server entry point against that session." --- -The store's account area has an orders page. -A request for it arrives with a `Cookie` header, and before the page renders or a server function returns a single order, the server has to answer two questions: who is this, and may they see this record. - +Before an account page renders or a server function returns an order, the server has to answer two questions: who is this, and may they see this record? Solid supplies the HTTP exchange those answers are built on: the incoming request, a `locals` bag, and the outgoing response headers, all on the request event. It does not supply a session store or an authentication framework. The `fullstack` template composes a signed cookie from `@remix-run/cookie` with that event, and this page builds the account area on the same shape. diff --git a/src/routes/(3)building-apps/(5)environment.mdx b/src/routes/(3)building-apps/(5)environment.mdx index 230eee980..d862d3055 100644 --- a/src/routes/(3)building-apps/(5)environment.mdx +++ b/src/routes/(3)building-apps/(5)environment.mdx @@ -4,10 +4,10 @@ version: "2.0" description: "Declare environment variables once, read secrets on the server and public values in the browser through typed modules, and let the build fail when a secret would leak." --- -The session cookie from [Sessions and auth](/building-apps/sessions-and-auth) is signed with `SESSION_SECRET`, and the store's header shows a name that changes between the staging and production deploys. -The first value must never reach the browser; the second is fine to ship and useful to have typed. +Some environment variables must never reach the browser, like the `SESSION_SECRET` that signs the cookie from [Sessions and auth](/building-apps/sessions-and-auth). +Others are fine to ship, like a store name that differs between staging and production. Vite's `import.meta.env` handles the second kind. -Start mode adds a layer that handles both, with one schema, two import paths, and a build that refuses to cross them. +Start mode adds a layer that handles both, with one schema, two import paths, and a build that fails if server values leak into client code. This page uses the two variables the `fullstack` project shape declares. The layer belongs to start mode and is not enabled by `ssr: true` alone. diff --git a/src/routes/(3)building-apps/(7)deployment.mdx b/src/routes/(3)building-apps/(7)deployment.mdx index 4afd2f5bd..6c54c6d8b 100644 --- a/src/routes/(3)building-apps/(7)deployment.mdx +++ b/src/routes/(3)building-apps/(7)deployment.mdx @@ -4,10 +4,8 @@ version: "2.0" description: "Put a start-mode build on a host: serve dist/client as static files, route everything else to the built request handler, supply the server environment at boot, and pick a Node server or a provider adapter." --- -The first deploy of a `fullstack` project usually fails in one of two ways. -The HTML arrives but every `/assets/*.js` and `.css` request returns 404, so the page is unstyled and never hydrates. -Or the home page works and refreshing `/account/orders` returns the host's 404 page. -Both mean the host is serving one half of the build: `dist/client` is a directory of static files, `dist/server/server.js` is a request handler, and the host has to put the first in front of the second. +A server-rendered build has two halves: `dist/client` is a directory of static files, and `dist/server/server.js` is a request handler. +The host has to serve the first in front of the second, and most first-deploy failures (unstyled pages, 404s on refresh) come from serving only one of them. A `bare` or `basic` project builds to `dist/client` alone, and any static host serves it. This page is about a project with `ssr: true` or server functions, which builds to static assets plus a server handler. diff --git a/src/routes/(4)routing/(0)overview.mdx b/src/routes/(4)routing/(0)overview.mdx index 0c930682d..1c8247d9f 100644 --- a/src/routes/(4)routing/(0)overview.mdx +++ b/src/routes/(4)routing/(0)overview.mdx @@ -4,10 +4,8 @@ version: "2.0" description: "Choose between Solid Router and TanStack Router for a Solid app, mount the router in App, and decide whether routes come from an array or from files under src/routes." --- -The store from the [Quick start](/getting-started/quick-start) has one page. -It needs a product page whose URL carries the product id, a cart at `/cart`, and a header that stays on screen while the page under it changes, and clicking between those pages must not reload the document. - -Solid does not ship a router. +The store from the [Quick start](/getting-started/quick-start) has one page, and it now needs a product page, a cart, and a header that stays on screen while the page under it changes, without reloading the document. +Solid does not ship a router for that. Routing is an integration: a router owns URL matching, navigation, and rendering the matched page, and the application renders it as part of the component tree like any other component. Two routers are supported, and both are mounted the same way. This page helps you pick one and shows the smallest routed app; the router-specific pages take over from there. 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 10f36f0db..67969df7c 100644 --- a/src/routes/(4)routing/(1)solid-router/(0)index.mdx +++ b/src/routes/(4)routing/(1)solid-router/(0)index.mdx @@ -4,12 +4,10 @@ version: "2.0" description: "Build a small routed store with Solid Router: a product page that reads its id from the URL, a header that stays mounted, typed links, and route data that starts loading before the page renders." --- -The store has one page. -It needs a product page at `/products/mug`, a header that stays put while pages change under it, links that do not reload the document, and product data that starts loading as soon as a link is clicked rather than after the page renders. - -Solid Router is the router the `basic` and `fullstack` project shapes ship with, published as `@solidjs/router`. +The store needs a product page at `/products/mug`, a header that stays put while pages change, links that do not reload the document, and product data that starts loading as soon as a link is clicked. +Solid Router handles all four, and it is the router the `basic` and `fullstack` project shapes ship with, published as `@solidjs/router`. It is optional: a Solid application can use [another router](/routing/overview#pick-a-router) or none. -This page builds the store above in four steps, using the parts an application touches most days; the pages after it cover each part in depth. +This page builds those four in order, using the parts an application touches most days; the pages after it cover each part in depth. ## One app, three pages diff --git a/src/routes/(4)routing/(1)solid-router/(1)setup.mdx b/src/routes/(4)routing/(1)solid-router/(1)setup.mdx index ce294c2e5..c4d33bc5e 100644 --- a/src/routes/(4)routing/(1)solid-router/(1)setup.mdx +++ b/src/routes/(4)routing/(1)solid-router/(1)setup.mdx @@ -4,8 +4,8 @@ version: "2.0" description: "Add Solid Router to a project that does not have it, mount one router at the application root, and set the options for a base path, hash history, or preloading." --- -A project created from the `bare` shape has an `<a href="/products/mug">` in its header, and clicking it reloads the whole document: the cart signal resets, the scroll position is lost, and the page flashes white. -The router is what turns that click into a client-side navigation that keeps the app alive. +Without a router, clicking a link reloads the whole document: signals reset, the scroll position is lost, and the page flashes white. +The router turns that click into a client-side navigation. The `basic` and `fullstack` templates from `npm create solid` already install and mount Solid Router, so if you started from one of them, skip to [Configure the router](#configure-the-router) when you need to change an option. The rest of this page adds the router to a project that does not have it, such as the `bare` shape or an existing Vite app. diff --git a/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx b/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx index 8f041e86f..a2407be39 100644 --- a/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx +++ b/src/routes/(4)routing/(1)solid-router/(2)route-definitions.mdx @@ -4,11 +4,8 @@ version: "2.0" description: "Write the route objects Solid Router matches against: path patterns with required, optional, and wildcard segments, typed parameters, match filters, metadata, lazy subtrees, and the file-system manifest." --- -The URL `/products/mug` has a product id in it. -The product page needs that id as `props.params.id`, the link that points at it needs `paths.products("mug")` to be checked by the type checker, and neither should accept `/products/` with nothing after it. -All of that comes from one object in the route tree. - -A route definition is a plain object: a path pattern, the component to render, and optionally a `preload`, `children`, match filters, and metadata. +The product page at `/products/mug` needs its id as `props.params.id`, and a link to it should be type-checked so `/products/` with no id is rejected. +Both come from the route definition, a plain object with a path pattern, the component to render, and optionally a `preload`, `children`, match filters, and metadata. Most apps need only [Match path patterns](#match-path-patterns) and [Type a route at its definition](#type-a-route-at-its-definition); the sections after those cover filters, metadata, lazy subtrees, and the file-system manifest for when a project grows into them. ```tsx diff --git a/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx b/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx index 16fa15d95..5d792ec04 100644 --- a/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx +++ b/src/routes/(4)routing/(1)solid-router/(4)navigation.mdx @@ -4,9 +4,9 @@ version: "2.0" description: "Link between pages with plain anchors, build URLs the compiler checks, navigate from code, read the location and search parameters, show active and pending links, and guard against leaving a page." --- -The first thing to look for in a new router is its `<Link>` component, and Solid Router does not have one. -A link is an `<a>`, and the router turns clicks on same-origin anchors inside it into client-side navigations. -That choice shapes this whole page: the URL is the API, and the router's job is to help you build URLs that are correct and react to the one the user is on. +Solid Router has no `<Link>` component. +A link is an `<a>`, and the router turns clicks on same-origin anchors into client-side navigations. +The URL is the API, and the router's job is to help you build correct URLs and react to the one the user is on. The examples continue the store from the [introduction](/routing/solid-router). 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 31e9e6672..d04351057 100644 --- a/src/routes/(4)routing/(1)solid-router/(5)data.mdx +++ b/src/routes/(4)routing/(1)solid-router/(5)data.mdx @@ -5,9 +5,12 @@ description: "Start route data early with preloads, cache and share reads with q --- 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. +That works, but it leaves three gaps, and the router's data APIs fill one each: + +- The request does not start until the page component runs. `preload` starts it before the component exists. +- A `ProductCard` in a list and the `Product` page fetch the same product twice. `query` gives both readers one request. +- The **Add to cart** form has no way to tell the cart it changed. A server function called from an `action` writes, and the router revalidates the cache from its response. `formAction` puts the same mutation behind a form that works without JavaScript. -The router's data APIs add one thing for each: `preload` starts a request before the page's component exists, `query` gives two readers one request, and a server function called from an `action` writes, with the router revalidating the cache from its response; `formAction` puts the same mutation behind a form that works without JavaScript. This page continues the store: a product page and a cart. :::note[All of it is optional] 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 402a161b6..1af35a589 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 @@ -4,11 +4,8 @@ version: "2.0" description: "Render Solid Router on the server: read the URL from the request, hydrate query results without a second fetch, return fresh data from a mutation in one round trip, and keep forms working without JavaScript." --- -Open the network tab on a server-rendered product page and there is one request, for the document, and none for the product. -Submit the **Add to cart** form and there is one `POST`, after which the cart in the header is already up to date. -Turn JavaScript off and the same form still adds to the cart. - -None of that needs a change to the earlier pages. +With server rendering, a product page loads in one request with its data already in the HTML, a form submission is one `POST` that also refreshes the cart, and forms keep working with JavaScript off. +None of that needs changes to the earlier pages. The same `Router` from `src/router.ts` renders on both sides; there is no server variant to import. This page explains what the server side does to make those three things true, and where the switches are when you need to change them. diff --git a/src/routes/(4)routing/(2)tanstack/(0)index.mdx b/src/routes/(4)routing/(2)tanstack/(0)index.mdx index 86825589d..f061cd7a4 100644 --- a/src/routes/(4)routing/(2)tanstack/(0)index.mdx +++ b/src/routes/(4)routing/(2)tanstack/(0)index.mdx @@ -10,7 +10,7 @@ The `fullstack-tanstack` project shape is that combination: the `fullstack` shap [TanStack Router](https://tanstack.com/router) owns routing and [TanStack Query](https://tanstack.com/query) owns the data cache, while Solid's server functions, streaming SSR, single-flight mutations, sessions, and API routes work unchanged. Nothing on the server asks which router is rendering. -This page explains how the three fit together in that template, so you can read its source with the design in mind and change it with confidence. +This page explains how the three fit together in that template. For the router's own API, route files, `<Link>`, loaders, search params, and so on, use the [TanStack Router documentation](https://tanstack.com/router/latest/docs/framework/solid/overview); nothing there changes under Solid. :::note[If you have not chosen a router yet] diff --git a/src/routes/(5)guides/(0)thinking-in-solid.mdx b/src/routes/(5)guides/(0)thinking-in-solid.mdx index d0da94b3a..45918a01c 100644 --- a/src/routes/(5)guides/(0)thinking-in-solid.mdx +++ b/src/routes/(5)guides/(0)thinking-in-solid.mdx @@ -4,10 +4,9 @@ version: "2.0" description: "Build the storefront's product search end to end, and at each step see what a React or Vue developer would reach for and what Solid does instead." --- -The storefront needs a search page: a text box, a category filter, a list of matching products from the server, and an **Add to cart** button on each row that updates the cart badge in the header. -If you have built this in React or Vue, you already have a plan: state for the query, a memoized filter, an effect that fetches, a loading flag, and a state update after the request succeeds. - -This guide builds the same page in Solid and stops at each step to say which part of that plan does not carry over. +This guide builds a product search page: a text box, a category filter, a list of matching products from the server, and an **Add to cart** button on each row that updates the cart badge in the header. +If you have built this in React or Vue, you already have a plan for it: state for the query, a memoized filter, an effect that fetches, a loading flag, and a state update when the request succeeds. +At each step, this guide points out which parts of that plan do not carry over to Solid. The [Quick start](/getting-started/quick-start) showed the one rule everything here follows: a component runs once, and a read inside JSX, a memo, or an effect's compute function is what updates. ## Start from the markup diff --git a/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx b/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx index cbf6be77a..9b4095c8e 100644 --- a/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx +++ b/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx @@ -4,17 +4,16 @@ version: "2.0" description: "Recognize the effect that copies one reactive value into another, replace it with a direct, memoized, async, or writable derivation, and keep createEffect for the imperative boundary." --- -The cart on the [Stores](/concepts/stores) page shows a subtotal under the line items. -The first version many people write keeps the subtotal in its own signal and adds an effect that recalculates it whenever an item changes. -It works, and it is wrong in a way that is hard to see: change a quantity and the row updates in one flush while the subtotal updates in the next, so for one frame the page shows the new quantity next to the old total. -With attribution on, development names the pattern `[EFFECT_RELAY_TEAR]`. +A common mistake is keeping a derived value, like a cart subtotal, in its own signal and updating it from an effect. +It looks like it works, but the row updates in one flush and the subtotal in the next, so for one frame the page shows the new quantity next to the old total. +With attribution on, development reports this as `[EFFECT_RELAY_TEAR]`. Signals, stores, props, memos, and async computations already form a graph of derived values. An effect belongs at the end of that graph, where a settled result has to leave Solid and drive something Solid does not own: ![Reactive inputs flow through derived values to declarative consumers and terminal effects. User interactions and external observations use setters to become new inputs.](/images/diagrams/derived-state-effects-sequence.svg) -This guide goes through the places an effect gets written where a derivation belongs, shows what each one does when it runs, and ends with the two cases where `createEffect` is the right tool. +This guide covers the common places people write an effect where a derivation belongs, and ends with the two cases where `createEffect` is the right tool. The [Reactivity](/concepts/reactivity) page explains tracking, memos, effect phases, scheduling, and ownership. ## Calculate values when they are read diff --git a/src/routes/(5)guides/(10)debugging-reactivity.mdx b/src/routes/(5)guides/(10)debugging-reactivity.mdx index cd787877b..28844fb1e 100644 --- a/src/routes/(5)guides/(10)debugging-reactivity.mdx +++ b/src/routes/(5)guides/(10)debugging-reactivity.mdx @@ -4,10 +4,8 @@ version: "2.0" description: "Find out why a value does not update, why it updates too often, and how to read Solid's development diagnostics." --- -A quantity input in the cart changes and the subtotal next to it does not. -Or the subtotal changes, and so does every row in the list, on every keystroke. -Those are the two shapes a reactivity bug takes: something does not update when it should, or something updates far more often than it should. -Solid's development build reports the first kind as it happens and can explain the second kind on request. +Most reactivity bugs are one of two kinds: something does not update when it should, or something updates far more often than it should. +Solid's development build warns about the first kind as it happens, and can explain the second kind when you ask. This guide shows how to read those reports and what to change. :::note[Development build only] @@ -26,12 +24,13 @@ Move it into a tracking scope (JSX, a memo, or an effect's compute function). at http://localhost:5173/src/components/LineItem.tsx:12:5 ``` -The code is stable across releases; the sentence after it says what the runtime observed and what to change. -The `in` line is the chain of owners from the root down to the scope that produced the finding: components as `<Name>`, computations by the `name` option you gave them or `effect`/`computed` by default. -The `at` line is the source position of the nearest node on that chain the compiler recorded, `file:line:col` relative to the project root; in a browser it is printed under the dev server's origin, so clicking it opens the file in the Sources panel. -When the finding is about a JSX binding (an attribute, class, style, or inserted text), the console entry also carries the DOM element it writes as a second argument, so hovering it in the browser's console highlights the element on the page and clicking it jumps to the Elements panel. +Codes are stable across releases, and the sentence after the code says what went wrong and what to change. -The first time each code appears, Solid adds a footer: a link to the code's page in the [diagnostics reference](/reference/solid-js/diagnostics), then the repair guide that ships inside the `solid-js` package under `skills/reactivity-diagnostics/SKILL.md` and the same file on GitHub, anchored to that code, for an assistant working in the repository. +- The `in` line is the chain of owners down to the scope that triggered the finding. Components show as `<Name>`, and computations by their `name` option, or `effect`/`computed` if they do not have one. +- The `at` line is the source position, `file:line:col`. In the browser, clicking it opens the file in the Sources panel. +- When the finding is about a JSX binding (an attribute, class, style or inserted text), the entry also includes the DOM element. Hover it in the console to highlight it on the page. + +The first time a code appears, Solid also prints a link to its page in the [diagnostics reference](/reference/solid-js/diagnostics) and to its section in the repair guide that ships in `solid-js` at `skills/reactivity-diagnostics/SKILL.md`, which is handy if you are working with an AI assistant. Codes fall into a few groups: @@ -47,11 +46,13 @@ Codes fall into a few groups: Not every bracketed message is a diagnostic. `[FLUSH_IN_ACTION]` is an error `flush()` throws in development; it never reaches the diagnostics channel, and [The test sees the old DOM](#the-test-sees-the-old-dom) covers it. -Each finding has one of three severities. -An error throws and halts execution; the behavior is broken. -A warning logs to the console; the code runs but is structurally wrong or expensive. -An `info` finding does not reach the console at all; it is recorded on the structured channel that `OBSERVE.diagnostics.subscribe()` and `@solidjs/diagnostics` read, for cases where the runtime has a lead but not enough proof to interrupt you. -Do not silence a code you do not understand; each one describes a real defect or a real cost. +Each finding has a severity: + +- An **error** throws and halts execution. The behavior is broken. +- A **warning** logs to the console. The code runs, but it is structurally wrong or expensive. +- An **info** finding never reaches the console. It goes to the structured channel that `OBSERVE.diagnostics.subscribe()` and `@solidjs/diagnostics` read, for cases where the runtime suspects something but is not sure enough to interrupt you. + +Do not silence a code you do not understand. Each one points at a real bug or a real cost. ## Something does not update @@ -132,8 +133,8 @@ See [Boundaries](/concepts/boundaries). ## Something updates too often -The tool for this is attribution: a recording of every scope that re-ran, what changed to cause it, and how long it took. -Give the scopes you care about names, enable attribution, reproduce the behavior, then ask why a scope ran: +Use attribution, which records every scope that re-ran, what caused it, and how long it took. +Name the scopes you care about, enable attribution, reproduce the problem, then ask why a scope ran: ```ts import { createMemo, createStore } from "solid-js"; @@ -154,11 +155,10 @@ for (const event of why(total)) { Each entry names the write that started the update, the dependency that changed, and the time the recompute took. While attribution is enabled the same chains print to the console as collapsed `[why-run]` groups, one per re-run, with the cause chain and dependency changes inside. -`costs()` ranks scopes by self time, including time spent on runs whose result did not change, and ranks writes by how much downstream work each one caused. -`why`, `costs`, `feedback`, and `subscriptions` are separate exports of `solid-js/attribution`, so a build that only records ships none of them; the `attribution` object itself carries `enable()`, which returns a release for the hold it took, `disable()`, `history(type)` for the `"rerun"`, `"hold"`, `"interaction"`, `"navigation"`, and `"waterfall"` ring buffers, and `markFlight()`. -The records themselves arrive on `OBSERVE.records.subscribe(type, (event, live) => …)`, the same channel the runtime's own records use, with the node that ran as `live`. -A `"rerun"` record is built only while something wants it — a `"rerun"` listener, an imported fold such as `costs`, or the console log — so `why(total)` answers for the runs after one of those appeared; `subscriptions(total)` reads the live graph and always answers. -To see the same records on a timeline instead of at the console, [`enablePerformanceTracks()`](/reference/solid-web/performance-tracks/enable-performance-tracks) from `@solidjs/web/performance-tracks` paints them in the Chrome Performance panel; [Performance](/guides/performance#see-the-records-on-the-performance-panel) shows the tracks. +`costs()` ranks scopes by self time (including runs whose result did not change) and writes by how much downstream work each one caused. +`why()` only knows about runs that happened after something started recording them (a `"rerun"` listener, an imported fold like `costs`, or the console log), while `subscriptions(total)` reads the live graph and always answers. +To see the same records on a timeline, [`enablePerformanceTracks()`](/reference/solid-web/performance-tracks/enable-performance-tracks) paints them in the Chrome Performance panel; see [Performance](/guides/performance#see-the-records-on-the-performance-panel). +The [attribution reference](/reference/solid-js/advanced/diagnostics-dev-hooks/attribution) lists everything else the module exports. A chain ends at whatever made the root write, not only at the signal's name. Writes made inside a compiled event handler are stamped with the interaction (`click on button#add "Add"`), so `event.interaction` on a re-run names the click it traces back to, however many effects relayed it in between. @@ -274,7 +274,7 @@ A `Loading` boundary that has not shown content yet, or whose `on` dependencies A boundary that has already revealed holds like everything else. A boundary with `on` whose fallback can never appear, because the same data is also read outside it, is reported on its own as `[LOADING_ON_OUTSIDE_HOLD]`, naming the source; move that outside read under the boundary. A hold that was acknowledged but still ran long is reported as `[LONG_HOLD]` from 500ms of waiting, a warning from 1000ms, with the suggestion to add a `Loading on={...}` boundary; the thresholds are `attribution.enable({ longHolds: { infoMs, warnMs } })`. -Acknowledged holds under those thresholds still count in the feedback tables: `feedback().sources` tallies every hold and its wait in `holds` and `heldMs`, ranks the affordances that answered in `acknowledgedBy`, and counts a hold whose only acknowledgment was a `latest()` shadow as `latestOnly`, next to the `silent` and `long` columns, so the cost is visible without blaming code that waited correctly. +Holds that do not trigger a warning still show up in `feedback().sources`, so you can see their cost without Solid blaming code that waited correctly. :::deep-dive[Feedback tables, and how a router names its holds] `attribution.history("hold")` returns every hold from the session, reported or not, each with `silent` and `long` already judged. @@ -331,8 +331,8 @@ Development throws; production skips the drain and runs the callback, if any, in Server render is pure: state changes flow from async sources (promises, async iterables), never setters. ``` -A server render is one pass from inputs to HTML. -A setter called during that pass lands as inert data and nothing updates, so the write is a sign the code expects a client-side update loop that does not exist on the server. +A server render is one pass from inputs to HTML, so a setter called during it does nothing. +The warning means the code expects a client-side update that never happens on the server. The fix depends on what the write was for: - Bridging a subscription or a promise into a signal: make the source itself the value, `createSignal(() => source)` or `createStore(async () => ..., seed)`, so both server and client read it the same way. diff --git a/src/routes/(5)guides/(11)performance.mdx b/src/routes/(5)guides/(11)performance.mdx index a0a782921..d4761d1c0 100644 --- a/src/routes/(5)guides/(11)performance.mdx +++ b/src/routes/(5)guides/(11)performance.mdx @@ -4,21 +4,21 @@ version: "2.0" description: "Measure a slow catalog page with the browser profiler and Solid's attribution tables, then fix the cause each measurement points at: hot derivations, wide writes, rebuilt rows, waterfalls, unsplit code, and a blocked streaming shell." --- -The catalog page renders 2,000 products. -Typing in its search box lags a beat behind each keystroke, and the first paint of the page waits until the product list, the cart badge, and the account menu have all answered. -Someone suggests memoizing everything; someone else suggests a virtual list. - -Both may be right, and neither is worth doing before a measurement says which part is slow. -This guide is about taking that measurement and reading which knob it points at. -It does not make speed claims; it describes what Solid does and does not do when a value changes, so the profile can be read against it. +Measure before you optimize. +Components in Solid do not re-render, so a slow page is rarely a component running too often. +It is usually one derivation doing too much work, one write reaching thousands of readers, a list rebuilding rows it could keep, or requests that wait on each other. +This guide shows how to find which one with the browser profiler and Solid's attribution tools, and what to change for each. +The running example is a catalog page with 2,000 products and a search box. ## Measure before changing Start in the browser's Performance panel: record, type one character in the search box, stop. -A long task after the keystroke shows the call stack that spent the time, and `performance.mark()` calls around suspect code give the flame chart named spans to look for. -That tells you where the milliseconds went; the next tool tells you why that code ran. +A long task after the keystroke shows the call stack that spent the time. +Wrap suspect code in `performance.mark()` calls to get named spans in the flame chart. -Attribution is Solid's development-build recorder of every scope that re-ran, what changed to cause it, and how long it took: +The profiler tells you where the time went. +Attribution tells you why that code ran. +It is a development-build recorder that logs every scope that re-ran, what caused it, and how long it took: ```ts import { attribution, costs, feedback } from "solid-js/attribution"; @@ -30,15 +30,24 @@ console.table(costs().scopes); console.table(feedback().flights); ``` -`costs().scopes` ranks scopes by `selfMs`, with `wastedMs` for the time spent on runs whose result did not change, and `costs().writes` ranks root writes by the downstream re-run time each one caused. -`feedback().flights` counts, per async source, how many requests started and how many were `abandoned` before landing; a high abandon count is the request-per-keystroke signature. -`feedback().fallbacks` counts how often each `Loading` boundary showed its fallback and how many of those were `flashes` under 150ms. +- `costs().scopes` ranks scopes by `selfMs`. `wastedMs` is the time spent on runs whose result did not change. +- `costs().writes` ranks writes by how much re-running each one caused downstream. +- `feedback().flights` counts, per async source, how many requests started and how many were `abandoned` before they landed. A high abandon count usually means a request per keystroke. +- `feedback().fallbacks` counts how often each `Loading` boundary showed its fallback, and how many of those were `flashes` under 150ms. + +Name your scopes before you measure: `createMemo(fn, { name: "visible" })`, `createStore(value, { name: "catalog" })`. +The tables and `why()` chains use those names, and an unnamed memo prints as `computed`. -Name the scopes first: `createMemo(fn, { name: "visible" })`, `createStore(value, { name: "catalog" })`. -Every table and every `why(visible)` chain refers to nodes by that name, and an unnamed memo prints as `computed`. +While attribution is on, Solid also warns when a scope is hot or wide: -While attribution is enabled, Solid also warns on its own when a scope re-runs 120 or more times in one second (`HOT_SCOPE_RERUNS`), spends 8ms or more of compute in one second (`HOT_SCOPE_TIME`), tracks 200 or more sources (`WIDE_SCOPE_DEPS`), or when one write reaches 250 or more subscribers (`HUGE_FAN_OUT` at the engine's `fanOut` threshold, with `data.write` naming the write). -Two warnings are always on in development: `HUGE_FAN_OUT` from 2,000 subscribers and `HUGE_FAN_IN` from 2,000 sources. +| Code | Fires when | +| ------------------ | ---------------------------------------------------------- | +| `HOT_SCOPE_RERUNS` | a scope re-runs 120+ times in one second | +| `HOT_SCOPE_TIME` | a scope spends 8ms+ of compute in one second | +| `WIDE_SCOPE_DEPS` | a scope tracks 200+ sources | +| `HUGE_FAN_OUT` | one write reaches 250+ subscribers (`data.write` names it) | + +Two warnings are on in development even without attribution: `HUGE_FAN_OUT` at 2,000 subscribers and `HUGE_FAN_IN` at 2,000 sources. [Debugging reactivity](/guides/debugging-reactivity#something-updates-too-often) explains how to read each report. :::note[Development and observe builds only] @@ -59,31 +68,27 @@ const release = enablePerformanceTracks(); release(); ``` -Record a profile, type one character, and the group shows a wave on **Propagation** labelled by the write and what it reached, `query "m" → "mu" — input on input#search · 2,003 runs, 2,000 unchanged`, with every run inside it beneath, labelled by what made it run. -**Effects** and **Memos** carry one span per re-run, colored by self time and marked as a warning when the run changed nothing; **Async** shows each request from kickoff to landing, with abandoned ones as warnings, and each `Loading` fallback from shown to hidden; **Holds** shows each wait labelled by its blockers; **Interactions** shows the keystroke from input delay through the handler to settle; **Navigations** shows each route change; **Server** shows the page's server-function calls and, beneath each, the server's own time for it. -Every finding while the tracks are on is also a marker on the panel's Timings track, `HOT_SCOPE_RERUNS — <Catalog>`, and a warning or worse is annotated for the Insights sidebar with a link to the code's repair guide. -The cost findings, such as `HOT_SCOPE_RERUNS`, appear only while some hold runs the cost checks: the console session above does, and the tracks' own hold does not unless asked. - -A label starts at the nearest component you wrote, `<Catalog> › visible` rather than the full path from the document root, because the panel elides the middle of a label that does not fit. -Solid's own flow components (`Show`, `For`, `Loading`, and the rest) never anchor a label, so a node under one reads `<Card> › <Show> › effect`; the unshortened path is the span's `Owner path` property, `Node` its runtime name, and in dev the span's tooltip is the same `[why-run]` chain the console prints. -Give scopes names before you record here for the same reason as above: an unnamed memo is `computed` on the track as well. +Record a profile and type one character. +The **Propagation** track shows the write and everything it reached, labelled like `query "m" → "mu" — input on input#search · 2,003 runs, 2,000 unchanged`. +**Effects** and **Memos** show one span per re-run, flagged when the run changed nothing. +**Async** shows each request from start to landing (abandoned ones are flagged) and each `Loading` fallback from shown to hidden. +**Interactions**, **Navigations**, **Holds** and **Server** fill in the rest: the keystroke from input delay to settle, route changes, what each held update waited on, and server time for each server-function call. +Diagnostics also show up as markers on the panel's Timings track. -The **Server** track reads the response's `Server-Timing` header, which a dev server writes for every request and an observe server writes while something on the server subscribes to the matching record: `solid-invocation` from an `"invocation"` record on a server-function response, and on a document `solid-shell` from the `"render"` record plus one `solid-boundary` per `Loading` boundary the shell waited on. -The document's own metrics appear as `shell · server` and `boundary <App> › <Loading> · server` when the tracks enable, and a call's appear beneath its span once the browser's resource timing places it. +Labels start at the nearest component you wrote, like `<Catalog> › visible`, and an unnamed memo shows up as `computed` here too, so name your scopes first. -`enablePerformanceTracks()` takes its own records-only hold on the engine, with `log: false` and `checks: false`, so it coexists with the console session above or with an adapter, and the console stays quiet only while nothing else asks for the log. -Pass `enablePerformanceTracks({ attribution: { checks: true } })` to run the cost checks (`hotRuns`, `hotTime`, `wideDeps`, `unstableMemos`, `fanOut`, `wastedRecompute`) under the tracks' own hold. -It accepts other `attribution` options for that hold, `minMs` to floor the spans it paints (`0` in dev, `0.05` in the observe build), and `rich` to switch between `performance.measure` entries with tooltips and properties and plain `console.timeStamp` entries. -An observe build's tracks inherit the engine's `values: "none"`, so no value previews and no element text reach the panel; pass `attribution: { values: "labels" }` for the control labels, or in dev `"none"` for a timeline without user data. +The tracks do not run the cost checks (`HOT_SCOPE_RERUNS` and friends) unless you ask for them with `enablePerformanceTracks({ attribution: { checks: true } })`. +They coexist with the console session above and with any observability adapter. The [`enablePerformanceTracks` reference](/reference/solid-web/performance-tracks/enable-performance-tracks) lists the options. ## What does not re-run A component function runs once. The JSX it returns compiles to expressions that each track what they read, so a signal write re-runs the expressions that read that signal and nothing else. -A component that runs too often is therefore not a failure mode to look for, and a profile that shows a component function high in the stack is showing its first and only run. +So do not go looking for a component that runs too often. +If a component function shows up in a profile, you are looking at its first and only run. -The failure modes that do exist are narrower, and each has a name in the console: +The real problems are narrower, and each one has a name in the console: | What the profile shows | Code | Fix | | ------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------- | @@ -147,7 +152,7 @@ When a memo must return an object or array that is often equal in content, pass ## Lists -A list is where one mistake is paid once per row. +In a list, every mistake is multiplied by the number of rows. `For` keeps a row when the same object comes back and rebuilds it when a new object appears, so the expensive version of a list is the one that hands `For` new objects for the same records: ```tsx @@ -273,7 +278,7 @@ For a record map whose rows are replaced wholesale rather than edited, `createSt ### Typing in the search box lags -Three causes, and attribution separates them. +Attribution tells you which of three causes it is. If `costs().scopes` puts a filter or sort at the top, it runs in every reader; make it a memo and put an equality boundary on the normalized query, as in [Expensive derivations](#expensive-derivations). If `feedback().flights` shows most requests abandoned, a request starts per keystroke; debounce in the input handler, as [Search as you type](/guides/data-fetching-patterns#search-as-you-type) shows. If the input itself shows the previous character, it is bound to `query()` while a held update waits on results; bind `latest(query)` instead. diff --git a/src/routes/(5)guides/(12)typescript.mdx b/src/routes/(5)guides/(12)typescript.mdx index 7c426d95c..f6142c04b 100644 --- a/src/routes/(5)guides/(12)typescript.mdx +++ b/src/routes/(5)guides/(12)typescript.mdx @@ -4,12 +4,8 @@ version: "2.0" description: "Replace the any annotations in a storefront with the types Solid exports for props, children, accessors, store drafts, event handlers, refs, server function results, and typed routes." --- -The storefront compiles, and a search for `any` in `src` finds six of them: the props of `LineItem`, the `children` of `Panel`, the `query` passed into `SearchResults`, the draft inside a `setCart` callback, an `onInput` handler, and a `ref`. -Each one was written to silence an error whose message was not clear at the time. - -This guide goes through those six places and shows which exported type belongs in each, and what the type checker reports when the wrong one is used. -The [type reference pages](/reference/solid-js/types/component-types) list the exports; [Components and JSX](/concepts/components-and-jsx#children-and-composition) introduces `ParentProps`. -This guide starts from there and shows the types in the cart and product pages those pages built. +This guide shows which type to use for component props, children, signals and setters, stores, events and refs, custom elements, server functions and generic components, and what the checker says when you get it wrong. +The [type reference pages](/reference/solid-js/types/component-types) list every exported type. ## Set up diff --git a/src/routes/(5)guides/(13)integrate-non-solid-code.mdx b/src/routes/(5)guides/(13)integrate-non-solid-code.mdx index 8ada2c2eb..b432d3cf3 100644 --- a/src/routes/(5)guides/(13)integrate-non-solid-code.mdx +++ b/src/routes/(5)guides/(13)integrate-non-solid-code.mdx @@ -4,14 +4,10 @@ version: "2.0" description: "Drive a charting library, a map, a web component, or an analytics script from Solid state: get the DOM node at the right time, create the instance once, update it in an effect, and keep browser-only imports off the server." --- -The account area needs a chart of order totals, and the charting library wants a `<canvas>` and a `chart.update(data)` call. -The store locator needs a map from a library that reads `window` the moment it is imported. -The checkout form uses a `<date-picker>` custom element that takes an array of disabled dates, and the analytics script wants to know the current URL on every navigation. - -None of these know about signals. -Each one needs the same four things from Solid: a DOM node once it exists, a place to create the instance once, an effect that pushes settled values into it, and a cleanup that tears it down with the component. -This guide goes through those four things and the two directions state can flow, from Solid into the library and from the library back into Solid. -The [Custom primitives](/guides/custom-primitives) guide shows how to package the result once it works. +Charting libraries, maps, custom elements and analytics scripts do not know about signals. +To use one in Solid you need a DOM node once it exists, somewhere to create the library's instance once, an effect that pushes new values into it, and a cleanup that tears it down with the component. +This guide covers each of those, using a chart, a map, a `<date-picker>` custom element and an analytics script as examples, and shows how to get state back out of the library into Solid. +Once it works, [Custom primitives](/guides/custom-primitives) shows how to package it. ## Get a DOM node @@ -32,7 +28,9 @@ Run the `Avoid` version and the library throws on `undefined`, because the compo The element exists after the JSX has been evaluated; a `ref` callback runs at that moment, while the surrounding render is still being built. The [`ref` reference](/reference/solid-web/jsx-properties/ref) lists the accepted forms, and [Refs and directives](/concepts/components-and-jsx#refs-and-directives) shows how a directive factory owns setup for an element. -Two facts from those pages matter for every integration on this page: a `ref` callback runs untracked and without an owner, so it is not a place to create effects or register cleanup; and setup that needs the element in the document belongs in `onSettled`, which runs after the first render has settled. +Two things matter for every integration on this page. +A `ref` callback runs untracked and without an owner, so do not create effects or register cleanup in it. +And setup that needs the element to be in the document belongs in `onSettled`, which runs after the first render has settled. ## Create the instance once, update it in an effect diff --git a/src/routes/(5)guides/(14)testing.mdx b/src/routes/(5)guides/(14)testing.mdx index 702f9ecc3..2fd0f529b 100644 --- a/src/routes/(5)guides/(14)testing.mdx +++ b/src/routes/(5)guides/(14)testing.mdx @@ -4,12 +4,11 @@ version: "2.0" description: "Write a component test that clicks a button and asserts the label, run it in jsdom or a real browser, and test fullstack server code in a separate Node project." --- -The first test most people write for a Solid component clicks the counter from the Quick start and asserts the label. -The click fires, the assertion runs on the next line, and the DOM still says `Clicks: 0`. -Nothing is broken: Solid applies writes in a batch after the current code finishes, and the test asserted before the batch landed. -One call fixes it, and it is the one Solid-specific thing in a component test. +Testing a Solid component is mostly like testing any other DOM code. +The one difference is that Solid batches writes and applies them after the current code finishes, so if a test clicks a button and checks the DOM on the next line, it will still see the old value. +Call `flush()` before you assert. -Beyond that call, testing a Solid app is a question of picking the smallest environment that exercises the behavior under test: +Beyond that, pick the smallest environment that covers what you are testing: 1. Start component tests in jsdom. The `basic` template uses this configuration for component DOM tests. diff --git a/src/routes/(5)guides/(15)observability.mdx b/src/routes/(5)guides/(15)observability.mdx index e13ce5bfc..9ee56228b 100644 --- a/src/routes/(5)guides/(15)observability.mdx +++ b/src/routes/(5)guides/(15)observability.mdx @@ -4,14 +4,13 @@ version: "2.0" description: "Report the errors your boundaries catch in production, follow a request from the server into the browser, and see what each user interaction cost, without wrapping a component." --- -The checkout form throws inside an `Errored` boundary in production. -The fallback renders, the shopper reloads and tries again, and nothing tells you it happened: the browser's global error handler never saw the error, because the boundary caught it. -Or a click on "Place order" feels slow for some shoppers, and the browser's own timing says how long the page was unresponsive but not which write waited on what. +This guide covers what Solid tells you about a running production app: errors your boundaries caught, traces that follow a request from the server into the browser, and what a slow interaction was waiting on. -Solid's runtime carries the answers to both. -On every build, an error hook on each platform hears every failure the runtime handled, once, with the component that threw and the boundary that caught it. -On the observe build, the runtime also publishes what it did as plain records — a `Loading` boundary that waited on the server, a server-function call and the execution it caused, a user interaction and the writes it held — and carries a request's trace into the browser on its own. -This guide shows how to turn each of those on and what each one costs. +Error reporting works on every build. +When an `Errored` boundary catches an error, the browser's global error handler never sees it, so Solid gives you a hook on each platform that reports every failure the runtime handled, along with the component that threw and the boundary that caught it. + +Everything else needs the observe build. +It publishes what the runtime did as plain records (a `Loading` boundary that waited on the server, a server-function call, a click and the writes it held) and carries the request's trace into the browser for you. ## Three builds @@ -23,8 +22,9 @@ Solid ships three builds of every runtime package, selected by export condition. | observe | `observe` | prod, plus the records channel, the diagnostics channel, the attribution slot, and the server's trace slot. No console output, no checks. | object | undefined | | dev | `development` | observe, plus the development checks and the console reporter. Unminified. | object | object | -The builds nest: whatever works on the observe build works on the dev build. -In Solid's size suite, the observe build's cap for a small client-rendered app is 17.83 KB against 16.04 KB for the production build (brotli), and enabling the attribution engine on top raises the cap to 31.98 KB; the engine is a separate entry, so an observe build that never imports it never ships it. +The builds nest, so anything that works on the observe build also works on the dev build. +The observe build is a little larger than prod. +The attribution engine is much larger, but it is a separate import and only ships if you import it. Opt into the observe build in the Vite plugin: @@ -37,10 +37,12 @@ export default defineConfig({ }); ``` -`observe: true` adds the `observe` condition to every environment and turns on source names ([`solid.sourceNames`](/reference/vite-plugin-solid/options#sourcenames)), so component labels such as `<Checkout>`, binding names such as `span.textContent`, and primitive names such as `count` survive minification and the paths below name components rather than `computed`. -Source locations (the `locations` kind, the `at` line of a dev console report) stay off in an observe build unless you set `sourceNames: { locations: true }`. -Under `vite dev` the `development` condition still wins; the dev build is a superset, so what you observe there holds in production. -Without the plugin, set `resolve.conditions` in your bundler (or `node --conditions=observe` for an unbundled server) and pass `sourceNames: true` to the compiler yourself; in a dev build the option is on by default. +`observe: true` adds the `observe` condition to every environment and turns on [`sourceNames`](/reference/vite-plugin-solid/options#sourcenames). +That keeps names like `<Checkout>`, `span.textContent` and `count` through minification, so the paths below name your components instead of saying `computed`. +Source locations (the `at` line in a dev console report) stay off unless you set `sourceNames: { locations: true }`. + +Under `vite dev` you still get the dev build, which is a superset, so anything you see there holds in production. +Without the plugin, set `resolve.conditions` in your bundler (or `node --conditions=observe` for an unbundled server) and pass `sourceNames: true` to the compiler yourself. :::note[Errors report on every build] The two error hooks are part of the runtime, not of `OBSERVE`. @@ -49,9 +51,9 @@ A production app with no observability tooling at all still gets them; the obser ## Hear the errors your boundaries catch -In the browser, an error that reaches nothing halts the reactive system and is handed to `reportError`, which `window.onerror` and every error monitor already listen on. -An error a boundary caught takes the other road: the fallback renders, the app keeps running, and no global handler hears about it. -`configureClientErrors` is where the runtime reports that road: +In the browser, an uncaught error halts the reactive system and goes to `reportError`, which `window.onerror` and every error monitor already listen to. +An error a boundary catches does not: the fallback renders, the app keeps running, and no global handler hears about it. +`configureClientErrors` is how you hear about those: ```ts title="src/monitor.ts" import { configureClientErrors } from "solid-js"; @@ -67,7 +69,7 @@ configureClientErrors({ ``` Click "Place order" with a broken price and the `Errored` fallback appears; the hook fires once, with `ownerPath` reading `<App> › <Checkout> › <OrderSummary> › computed` and `boundaryPath` reading `<App> › <Checkout> › <Errored>`. -The two paths answer different questions: where the code broke, and what the shopper saw instead. +The first path is where the code broke; the second is what the shopper saw instead. A boundary's `reset()` recomputing the same failing node collects the same error object again and does not report it again. The paths are present where the runtime keeps owner names, which is the observe and dev builds; on the production build the hook still fires and the paths are `undefined`. @@ -75,8 +77,8 @@ A root can carry its own hook ahead of the ambient one: `render(App, el, { onErr ## Hear every failure the server handles -The server has more ways to handle a failure than the client, and a global handler sees none of them: an `Errored` fallback rendered into the stream, a `Loading` fragment that rejected and was handed to the client to re-render, a server function that threw, a hydration value that would not serialize, and the failure that fails the request. -`configureServerErrors` hears all of them, once per error object, with the site that met it: +On the server, a failure can be handled in several ways that no global handler sees: an `Errored` fallback rendered into the stream, a `Loading` fragment that rejected and was handed to the client, a server function that threw, a hydration value that would not serialize, or a failure that fails the whole request. +`configureServerErrors` hears all of them, once per error object: ```ts title="src/instrument.ts" import { configureServerErrors } from "@solidjs/web"; @@ -97,7 +99,7 @@ configureServerErrors({ }); ``` -`kind` and `handling` say which road the failure took: +`kind` and `handling` tell you what happened: | `kind` | `handling` | What happened | | ----------------- | ----------- | ----------------------------------------------------------------------------------------- | @@ -118,7 +120,7 @@ A monitoring SDK's `init()` has to run before the modules it patches load; the p :::danger[The return value goes on the wire] The hook receives the error as thrown. -What the client receives — the serialized error in an `Errored` fallback, the body of a failed server-function call — is the runtime's sanitized value, a generic `Error` outside the dev build. +What the client receives (the serialized error in an `Errored` fallback, or the body of a failed server-function call) is a sanitized value, a generic `Error` outside the dev build. Return a value from the hook and that value replaces it. Return the error itself and its message, stack, and any secret they carry reach the browser. Return a reference the shopper can quote back to you, or nothing. @@ -153,9 +155,10 @@ export async function chargeCard(orderId: string) { } ``` -The runtime also tells the browser which trace the page belongs to, on two carriers it already owns: a `Server-Timing` header on every response, and `<meta>` tags in an HTML shell's head, one per named entry. -A frame stream and a server-function response have no `<head>`, so the header is what makes those joinable; no middleware rewrites the document. -The browser is told when the incoming `traceparent` was sampled or when a provider answered; an unsampled or originated trace stays server-side, so a page with no tracing tool sees no change to its responses. +The runtime also tells the browser which trace the page belongs to, through a `Server-Timing` header on the response and `<meta>` tags in the HTML head. +Frame streams and server-function responses have no `<head>`, so they rely on the header. +You do not need middleware for any of this. +The trace only reaches the browser when the incoming `traceparent` was sampled or a provider answered, so a page with no tracing tool sees no change to its responses. On the observe build, a tracing tool provides the trace instead of the header: @@ -190,7 +193,7 @@ The fields it returns replace the runtime's derivation; its `entries` merge by n | `"frame"` | both | A frame stream produced (server) or applied (client). | | `"recovery"` | client | A `Loading` boundary the server gave up on, re-rendered as fresh DOM on the client; joins its `"boundary"` by id. | -The attribution engine's own records — `"interaction"`, `"hold"`, `"navigation"`, `"rerun"`, and the timeline records behind the [Performance panel tracks](/guides/performance#see-the-records-on-the-performance-panel) — arrive on the same channel once the engine is enabled, so an adapter has one `subscribe` to call. +Once the attribution engine is enabled, its records (`"interaction"`, `"hold"`, `"navigation"`, `"rerun"`, and the timeline records behind the [Performance panel tracks](/guides/performance#see-the-records-on-the-performance-panel)) arrive on the same channel. Log the boundaries that kept a shopper waiting: @@ -208,7 +211,7 @@ OBSERVE?.records.subscribe("boundary", (event) => { Render `/orders` with a slow database and the line reads `<Loading> at <App> › <Orders> › <Loading> waited 640ms settled`. A record is data: ids, names, an outcome, `at` on the `performance.now()` clock, durations, counts. -Anything live — the request, the response, the arguments, the error as thrown — travels in a second argument to the listener, never on the record, so a record can leave the process as it is. +Anything live (the request, the response, the arguments, the error as thrown) comes in a second argument to the listener, never on the record, so you can send a record off the device as it is. A client `"call"` and the server `"invocation"` it caused share an `id`; the difference between their durations is the wire. The `"request"` and `"call"` records of one call share their `live` object by identity, filled in as the call proceeds, so a listener holding it from the send reads the outcome off it at settle. Request and response bodies are taken only for a listener that asked, `subscribe("call", fn, { bodies: true })`; a listener that reads ids, statuses, and timings never pays for them. @@ -220,7 +223,7 @@ Subscribe from a module that loads before the app; the channel exists once per p ## What each interaction cost -The attribution engine answers the second question from the opening: a click felt slow, and on what did it wait. +When a click feels slow, the attribution engine tells you what it waited on. It is the same engine [Debugging reactivity](/guides/debugging-reactivity#something-updates-too-often) uses in development, and on the observe build it runs in production when you enable it: ```ts @@ -246,13 +249,14 @@ A hold names what blocked the write, how long, and whether the screen acknowledg A router that wraps its location write in `OBSERVE.attribution.withOrigin` gives its navigations the matched route pattern as their name, so `/orders/:id` folds together across shoppers; the route the document arrived on is declared the same way, so the first navigation has a name too, and a server `"render"` record carries it as `route`. `enable()` takes a hold on the engine and returns its release. -The engine is shared by every tool on the page — an adapter, the Performance panel tracks, a diagnostics capture — so each takes its own hold, options combine by the most demanding request per key, and the engine stays on until the last release; `attribution.disable()` tears everything down regardless. +Every tool on the page (an adapter, the Performance panel tracks, a diagnostics capture) shares one engine. +Each takes its own hold, options combine by the most demanding request, and the engine stays on until the last hold is released. +`attribution.disable()` tears everything down regardless. Record subscriptions belong to `OBSERVE.records`, not to the engine, so they survive a release and are dropped only by the function `subscribe` returned. `attribution.history("interaction")`, `history("hold")`, `history("navigation")`, and `history("rerun")` return the same records from a ring buffer for a console session that did not subscribe ahead of time. -The engine records; the tables are separate. `feedback()`, `costs()`, `why()`, and `subscriptions()` are their own exports of `solid-js/attribution`, so a build that only subscribes to records ships none of them. -A `"rerun"` record is built only while something wants it — a `"rerun"` listener, an imported fold, or the console log — so `history("rerun")` and `why()` are empty for runs before one appeared; `subscriptions()` reads the live graph and is unaffected. +A `"rerun"` record is only built while something wants it (a `"rerun"` listener, an imported fold, or the console log), so `history("rerun")` and `why()` are empty for runs before one appeared; `subscriptions()` reads the live graph and is unaffected. ## What leaves the process @@ -273,10 +277,10 @@ The first two are governed at the source by the engine's `values` option, so a r | `"none"` | absent | element only: `button#save` | observe build | Across holds the least permissive level wins, so a production adapter that passes `values: "none"` keeps it beside a panel asking for `"full"`. -Pass the level explicitly in a tool that exports records; it is the tool's contract, not a default to rely on. +If your tool exports records, pass the level explicitly instead of relying on the default. `to`, `from`, `params`, and `data.error` are outside the level: a tool that exports them treats them as it treats any URL or captured exception. -A tool that renders inside the app it watches — a diagnostics panel, devtools — marks its root as its own, so its effects and stores never appear as findings about the app: +A tool that renders inside the app it watches, like a diagnostics panel or devtools, marks its root as its own, so its effects and stores never appear as findings about the app: ```ts import { createRoot, getOwner, OBSERVE } from "solid-js"; diff --git a/src/routes/(5)guides/(16)observability-adapters.mdx b/src/routes/(5)guides/(16)observability-adapters.mdx index 5528c0670..99be1c061 100644 --- a/src/routes/(5)guides/(16)observability-adapters.mdx +++ b/src/routes/(5)guides/(16)observability-adapters.mdx @@ -4,10 +4,10 @@ version: "2.0" description: "Connect an error monitor or a tracing SDK to a Solid app through the runtime's hooks and channels, with nothing wrapped, patched, or rewritten." --- -You maintain an error monitor or a tracing SDK, and a team running Solid asks for an integration. -For most frameworks that means a component wrapper for error boundaries, a router integration for navigation spans, a middleware to inject trace headers into the HTML, and a per-host preload for the server. -Solid's runtime exposes each of those as a hook or a channel, so an adapter is one `init()` on each platform that subscribes. -This guide is the contract behind those hooks: what fires, when, with what, and what your listener may do. +This page is for people writing an error monitor or tracing integration for Solid. +With most frameworks that means a component wrapper for error boundaries, a router integration for navigation spans, middleware to inject trace headers, and a preload for the server. +In Solid, each of those is a hook or a channel the runtime already exposes, so an adapter is one `init()` per platform that subscribes to them. +What follows is the contract for those hooks: what fires, when, with what, and what your listener is allowed to do. :::advanced[For tool authors] An app author enabling an existing tool wants [Observability](/guides/observability). @@ -43,7 +43,7 @@ The records below carry the same error object beside a failed invocation or call Set a status on the span from the record; capture from the hook. On the server, a returned value replaces what the client receives. -An adapter that offers this — a reference the user can quote — owns the promise that nothing from the original error crosses the wire; an adapter that does not returns nothing. +If your adapter returns a reference the user can quote, it is responsible for making sure nothing from the original error crosses the wire. If it does not offer that, return nothing. ## Records: settled, serializable, delivered inside the runtime @@ -53,7 +53,7 @@ The contract that shapes a span builder: - A record is complete when you receive it, with `at` on the `performance.now()` clock and durations in milliseconds. Build the span after the fact with an explicit start and end: `epochSeconds = (performance.timeOrigin + at) / 1000`. - A record is plain data. - Anything live — the request, the response, the arguments, the result, the error as thrown — is the second argument, `live`, and stays in the process. + Anything live (the request, the response, the arguments, the result, the error as thrown) is in the second argument, `live`, and stays in the process. - The listener runs synchronously inside the runtime. It must not write signals; defer span creation to a microtask so the SDK's own bookkeeping never runs inside a Solid flush. A listener that throws is reported to the console and does not stop the others. @@ -154,7 +154,7 @@ Set `parentId` when your SDK continues from a different header than the runtime `OBSERVE.diagnostics.subscribe(listener)` delivers the runtime's findings: a hold nothing acknowledged (`SILENT_HOLD`), a scope re-running hot (`HOT_SCOPE_RERUNS`), a waterfall of dependent requests in the browser (`ASYNC_WATERFALL`) or of sequential waits inside one server boundary (`SSR_BOUNDARY_WATERFALL`), a render error the server contained (`SSR_RENDER_ERROR_CONTAINED`), an error replaced by a generic one on the wire (`SERVER_ERROR_SANITIZED`, with `data.source` naming the road: `"ssr"` or `"server-function"`). A finding has a stable identity and recurs, so it is an issue: fingerprint by `code` and `ownerPath`, and let the count grow. The listener's second argument is the live node the finding is about, when the runtime located one; like `live` on a record, it stays in the process, and `OBSERVE.ownerPath(subject)` reads the same labels the event carries. -Severity is the runtime's — `info`, `warn`, `error` — and `info` findings never reach the console in any build, so an adapter decides whether to forward them. +Severity is set by the runtime (`info`, `warn` or `error`), and `info` findings never reach the console in any build, so an adapter decides whether to forward them. `data.error` on the server's error findings is the error as thrown; the error hook has captured it already. ## Keep the adapter out of its own data diff --git a/src/routes/(5)guides/(17)ai-tools.mdx b/src/routes/(5)guides/(17)ai-tools.mdx index a43761de0..c9b50a51e 100644 --- a/src/routes/(5)guides/(17)ai-tools.mdx +++ b/src/routes/(5)guides/(17)ai-tools.mdx @@ -4,11 +4,10 @@ version: "2.0" description: "Point a coding assistant at the cheatsheet, skills, diagnostics, lint configs, and Markdown docs that ship with Solid 2.0, and paste a rules block into its instructions file." --- -An assistant asked to add a feature to a Solid 2.0 app writes code from what it learned elsewhere: props destructured in the signature, an effect with one function, a store written through a path setter, `Suspense` and `createResource`. -Each of those is wrong in 2.0, and some of them run without an error, so the mistake shows up as a value that never updates rather than as a stack trace. - -Solid 2.0 ships the material that corrects this, and most of it is installed with the packages so it matches the exact version in `node_modules`. -This page lists every piece, says what it is for, and ends with a block to paste into the assistant's instructions file. +AI assistants mostly learned Solid from 1.x code and from other frameworks, so they tend to destructure props, write single-function effects, and reach for APIs that 2.0 does not have. +Some of those mistakes run without an error and leave a value that never updates. +Solid 2.0 ships docs written for assistants inside its packages, so they always match the version in `node_modules`. +This page lists what is there and ends with a block to paste into your assistant's instructions file. ## What ships with the packages diff --git a/src/routes/(5)guides/(18)ai-sdk-chat.mdx b/src/routes/(5)guides/(18)ai-sdk-chat.mdx index 14f575b7e..3876d706a 100644 --- a/src/routes/(5)guides/(18)ai-sdk-chat.mdx +++ b/src/routes/(5)guides/(18)ai-sdk-chat.mdx @@ -4,15 +4,13 @@ version: "2.0" description: "Drive the AI SDK's chat class from a Solid store with a short ChatState adapter, so messages, status, and errors update the exact nodes that read them." --- -You have an API route that answers `POST /api/chat` with `streamText(...).toUIMessageStreamResponse()` from the `ai` package, and you want the chat screen in Solid. -The package's own framework adapters hold messages in their framework's state, and `@ai-sdk/solid` is deprecated: its README says so, names AI SDK 5 as the release that removes it, and its last version stops at the `useChat` hook of that era. +This recipe builds a chat screen in Solid on top of an API route that answers `POST /api/chat` with `streamText(...).toUIMessageStreamResponse()` from the [AI SDK](https://ai-sdk.dev). +`@ai-sdk/solid` is deprecated, but you do not need it. +`AbstractChat` in the `ai` package handles the transport, stream parsing, tool calls and status, and reads and writes messages through a small `ChatState` interface. +Implement that interface on a Solid store and you have a chat adapter in about sixty lines. +It does not need throttling, because a store update only touches the nodes that read the changed value. -Nothing in the chat logic depends on a framework, though. -`AbstractChat` in `ai` does the transport, the stream parsing, tool calls, and status; it reads and writes its messages through a small `ChatState` interface that the adapter supplies. -This recipe is that adapter on a Solid store, about sixty lines, with no throttle, because a store update already touches only the nodes that read the changed value. - -This recipe targets `ai` 7.x; the interface is quoted from its source. -Install the package with your package manager, no `@ai-sdk/*` UI package is needed. +This recipe targets `ai` 7.x, and only needs the `ai` package. ## The interface to implement @@ -107,8 +105,7 @@ export class Chat<M extends UIMessage = UIMessage> extends AbstractChat<M> { } ``` -Three decisions in that file are worth knowing about. - +A few choices in that file are worth explaining. `status` and `error` are signals and `messages` is a store, so a component that reads `chat.status` re-runs only when the status changes, and a component that reads one message's text re-runs only when that text changes. The stream processor calls `replaceMessage` once per chunk with a message whose last text part has grown. Assigning that object into the array slot would replace the row and re-run every reader of it, so the adapter passes the array through [`reconcile`](/reference/solid-js/stores/reconcile) instead: rows match by `id` and keep their proxies, the unchanged rows are skipped by reference, and inside the changed row only the `text` leaf that differs notifies its readers. diff --git a/src/routes/(5)guides/(2)custom-primitives.mdx b/src/routes/(5)guides/(2)custom-primitives.mdx index 98f1257ca..a61813392 100644 --- a/src/routes/(5)guides/(2)custom-primitives.mdx +++ b/src/routes/(5)guides/(2)custom-primitives.mdx @@ -4,13 +4,9 @@ version: "2.0" description: "Move repeated reactive setup into a createX function that runs inside an owner, accepts accessors, returns accessors, cleans up what it starts, and stays correct during server rendering." --- -The catalog page debounces its search query before it calls `searchProducts`. -The orders page switches from a table to cards below a viewport width. -The checkout page keeps a cart draft in `localStorage` so a refresh does not lose it. -Each of those is a signal, a timer or a listener, and a cleanup, and the same six lines now sit in three components with small differences between them. - -A custom primitive is a function that packages those lines: it creates signals, memos, and effects, and returns what the component needs to read. -Solid has no rules about where such a function may be called beyond the ones every primitive already follows, and this guide is about those rules. +The same few lines for a debounced query, a viewport check, or a cart draft saved to `localStorage` tend to end up copied across components. +A custom primitive is a function that packages them: it creates signals, memos and effects, and returns what the component needs to read. +Writing one follows the same rules as the built-in primitives, and this guide covers those rules. The [Reactivity](/concepts/reactivity) page explains tracking, effects, and ownership; this page assumes them. ## A primitive is a function that runs inside an owner diff --git a/src/routes/(5)guides/(3)state-management.mdx b/src/routes/(5)guides/(3)state-management.mdx index 21933e31f..d161d83b8 100644 --- a/src/routes/(5)guides/(3)state-management.mdx +++ b/src/routes/(5)guides/(3)state-management.mdx @@ -4,12 +4,9 @@ version: "2.0" description: "Decide where each piece of storefront state lives: a component, a context provider, the URL, or the server, and keep it out of module scope in code that also runs on the server." --- -The search page from [Thinking in Solid](/guides/thinking-in-solid) ended with a cart count created inside the `Search` component. -The header shows that count on every page, so it has to move. -The same question comes up for the signed-in customer's name, the search filter that should survive a refresh, the product catalog, and the open flag on the **Remove item?** dialog. - -Each of those has one right home, and the wrong home shows up as a symptom: a count that resets when the page changes, a filter that clears on refresh, a dialog that stays open after the user navigates away, or a cart that belongs to a different visitor. -This guide goes through the homes one at a time and ends with a decision list. +Where a piece of state lives decides how long it lasts and who can read it. +Put it in the wrong place and you get a cart count that resets when the page changes, a filter that clears on refresh, a dialog that stays open after navigating away, or one visitor's cart showing up for another. +This guide picks up the search page from [Thinking in Solid](/guides/thinking-in-solid), whose cart count now needs to move to the header, and goes through each place state can live. ## Five kinds of state diff --git a/src/routes/(5)guides/(4)forms.mdx b/src/routes/(5)guides/(4)forms.mdx index 3cec8ee4d..4d0f2958d 100644 --- a/src/routes/(5)guides/(4)forms.mdx +++ b/src/routes/(5)guides/(4)forms.mdx @@ -4,16 +4,12 @@ version: "2.0" 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. -The form has to save the address when the user clicks **Continue**, reject a postal code that is not five digits even when someone posts to it with `curl`, and show the user which field was wrong. -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 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. -Each pass is a working form on its own; stop at the one that matches what the app needs. +This guide builds a checkout shipping-address form in four steps, and each step is a working form you can stop at: + +1. A form that posts to a server function through a Solid Router form action, and works with no JavaScript. +2. Server-side validation the user cannot bypass, even by posting with `curl`. +3. Inline validation messages and a pending state, without changing the HTML. +4. An address book where the new entry shows up before the server confirms it. The guide assumes a `fullstack` project with [server functions](/building-apps/server-functions) enabled and [Solid Router](/routing/solid-router) mounted. diff --git a/src/routes/(5)guides/(5)lists.mdx b/src/routes/(5)guides/(5)lists.mdx index 5237f875c..ca6aaf6fa 100644 --- a/src/routes/(5)guides/(5)lists.mdx +++ b/src/routes/(5)guides/(5)lists.mdx @@ -4,14 +4,9 @@ version: "2.0" description: "Render, edit, filter, sort, select in, and window lists without rebuilding rows, and read the diagnostics that tell you when you are." --- -The cart on the [Stores](/concepts/stores) page has a quantity input in every row. -Change one quantity and the row you are typing in has to stay: the input keeps focus, a row animation keeps playing, the scroll position holds. -The first version of that list many people write rebuilds every row on every change, and the symptom is an input that loses focus after one keystroke. - -The same problem shows up in search results, order history tables, and chat threads. -A list is where a small mistake costs the most, because a row that is rebuilt instead of updated loses its DOM, its focus, its scroll position, and its animation, and does so once per row. - -This guide is organized by what you are doing with the list. +When a list row is rebuilt instead of updated, it loses its DOM, its focus, its scroll position and any running animation. +The usual symptom is an input that loses focus after one keystroke. +This guide shows how to keep rows stable, organized by what you are doing with the list. The [Components and JSX](/concepts/components-and-jsx#rendering-lists) page introduces `For` and `Repeat`; this page is about using them well. The examples use the cart's line items: diff --git a/src/routes/(5)guides/(6)data-fetching-patterns.mdx b/src/routes/(5)guides/(6)data-fetching-patterns.mdx index 69ce39fd6..8d9150417 100644 --- a/src/routes/(5)guides/(6)data-fetching-patterns.mdx +++ b/src/routes/(5)guides/(6)data-fetching-patterns.mdx @@ -4,12 +4,12 @@ version: "2.0" description: "Load data for search, detail pages, pagination, shared state, and dashboards without loading flags, request counters, or abort controllers, and decide where each request and each boundary goes." --- -The product page on the [Async reactivity](/concepts/async-reactivity) page loaded one product with one memo. -A storefront makes more requests than that: a search box that fires on every keystroke, a product page that also needs reviews and related items, an order history with pages, a cart badge that several components read, a dashboard that should stay current without a reload. -Each of those is usually written with a loading flag, a request counter, or an `AbortController`; in Solid each is a computation, because a computation that returns a promise is a value. +[Async reactivity](/concepts/async-reactivity) loaded one product with one memo. +This guide covers the other requests a real app makes: search as you type, a page that needs several things at once, pagination, data several components share, and data that stays live. +These are often written with a loading flag, a request counter or an `AbortController`. +In Solid each one is a computation that returns a promise. -This guide goes through those requests one at a time and shows the Solid shape for each, including the choices that are yours to make. -Each `api.*` call is a function that returns a promise; a `"use server"` function, a `fetch` wrapper, or a client SDK all work the same way. +Each `api.*` call below is any function that returns a promise: a `"use server"` function, a `fetch` wrapper, or a client SDK. ## Load one thing @@ -77,16 +77,13 @@ function ProductSearch() { ``` Type `mug` and three requests start, one per keystroke; the list shows the results for `mug` and nothing else. -Three things this code does not do, because Solid does them: - -- It does not discard stale responses by hand. - Only the answer to the current question is used; if the response for `mu` arrives after the one for `mug`, it is dropped. - There is no request counter and no `AbortController`. - The superseded request still finishes in the background; to cancel it, see [Stop the work a run started](/concepts/async-reactivity#stop-the-work-a-run-started). -- It does not blank the list between keystrokes. - The previous results stay visible with the `stale` class until the new ones land. -- It does not show a spinner for an empty query. - Returning `[]` synchronously is a settled answer, so the memo never becomes pending for it. +You do not need a request counter or an `AbortController`. +If the response for `mu` arrives after the one for `mug`, Solid drops it. +The old request still runs to completion; see [Stop the work a run started](/concepts/async-reactivity#stop-the-work-a-run-started) if you want to cancel it. + +The list does not go blank between keystrokes either. +The previous results stay up, with the `stale` class, until the new ones arrive. +And an empty query never shows a spinner, because returning `[]` synchronously is already a settled answer. The input binds `value={latest(query)}` rather than `value={query()}`. The write to `query` is held while results load, and `latest` reads the value the update is moving toward, so a controlled input reflects what the user typed. diff --git a/src/routes/(5)guides/(7)protected-routes.mdx b/src/routes/(5)guides/(7)protected-routes.mdx index aa76da630..15542aff1 100644 --- a/src/routes/(5)guides/(7)protected-routes.mdx +++ b/src/routes/(5)guides/(7)protected-routes.mdx @@ -4,17 +4,15 @@ version: "2.0" description: "Require a signed-in customer for the account area at the data, route, and request layers, redirect a signed-out visitor to sign-in on the server and in the browser, and send them back where they were going." --- -The account area from [Nested routes and layouts](/routing/solid-router/nested-routes) has three pages: `/account`, `/account/orders`, and `/account/addresses`. -A signed-out visitor reaches them three ways: a bookmark or a shared link, which is a full page request; the **Account** link in the header, which is a client-side navigation; and a script that calls `getOrders` directly, which never renders anything. -All three should end at `/sign-in`, and after signing in the visitor should land on the page they asked for, not on the account home. +A signed-out visitor can reach a protected page three ways: by loading the URL, by clicking a link in the app, or by calling its server functions directly without loading a page at all. +All three should end at `/sign-in`, and after signing in the visitor should land on the page they asked for. -[Sessions and auth](/building-apps/sessions-and-auth) built the cookie, `getSession()`, and the middleware that sets `event.locals.userId`. -This guide assembles those pieces into a guard that holds at every layer and uses the account area as the running example. +This guide protects the account area from [Nested routes and layouts](/routing/solid-router/nested-routes) (`/account`, `/account/orders` and `/account/addresses`), using the cookie, `getSession()` and middleware from [Sessions and auth](/building-apps/sessions-and-auth). -## Three layers, one decision +## Where the check goes -The decision is always the same: is there a `userId` on the request event. -Where the decision runs determines what it protects. +The check is always the same: is there a `userId` on the request event? +What it protects depends on where it runs. - A check inside each server function protects the data. It runs for a page render, a router navigation, a form post without JavaScript, and a `curl` command alike, because every one of those reaches the function through the same request event. @@ -23,7 +21,7 @@ Where the decision runs determines what it protects. - A check in middleware protects the request. It answers a full page request for `/account/*` with a redirect before any rendering starts. -Each alone is not enough. +You need all three. A route guard alone leaves `getOrders` callable by anyone who knows its URL, because a route guard decides what to render and never sees a direct call. The server function check alone stops the data and nothing else: in the browser the read receives a response object instead of a navigation, and the page is left without its data. Middleware alone never sees a client-side navigation, because a navigation inside the router makes no page request. diff --git a/src/routes/(5)guides/(8)ssr-safe-code.mdx b/src/routes/(5)guides/(8)ssr-safe-code.mdx index 0aa510bc8..771e81b90 100644 --- a/src/routes/(5)guides/(8)ssr-safe-code.mdx +++ b/src/routes/(5)guides/(8)ssr-safe-code.mdx @@ -4,12 +4,10 @@ version: "2.0" description: "Keep browser APIs, per-run values, and client-only data out of the server render, and read each hydration warning to find the line that made the server and the browser disagree." --- -The storefront passed `vite build`, and the first request to the dev server with `ssr: true` printed `ReferenceError: window is not defined` from the cart drawer. -Moving that line fixed the crash, and the browser console then filled with `Hydration structure mismatch` warnings from the sale banner, which the server rendered and the browser decided not to. - -Both problems have the same shape: one component source runs on the server and in the browser, and a line that gives a different answer in each place breaks one of them. -[Rendering and SSR](/concepts/rendering-and-ssr) explains the model. -This guide is the checklist for the code: where each line runs, which lines cannot run on the server, which values must not differ, and how to read the warnings when they do. +With server rendering on, the same component code runs on the server and in the browser. +Most SSR bugs come from a line that only works in one of them (`window is not defined`) or gives a different answer in each (`Hydration structure mismatch`). +This guide covers where each line runs, what cannot run on the server, what must not differ between the two, and how to read the warnings when something does. +[Rendering and SSR](/concepts/rendering-and-ssr) explains the model behind it. ## Where each line runs diff --git a/src/routes/(5)guides/(9)choose-a-rendering-mode.mdx b/src/routes/(5)guides/(9)choose-a-rendering-mode.mdx index f4d354b8a..2d7696ef5 100644 --- a/src/routes/(5)guides/(9)choose-a-rendering-mode.mdx +++ b/src/routes/(5)guides/(9)choose-a-rendering-mode.mdx @@ -4,10 +4,8 @@ version: "2.0" description: "Decide between a static shell, streaming server rendering, and build-time prerendering for a start-mode project, and what each one costs." --- -Someone shares a product page from the storefront in a chat, and the link preview shows the site name and nothing else. -The page is there when a browser opens it, but the preview scraper read the HTML response, and the HTML response was a document shell with an empty body waiting for a bundle. -Whether that is a problem depends on who reads the HTML, and that question is what picks a rendering mode. - +Which rendering mode you need mostly depends on who reads your HTML. +A browser can wait for JavaScript to fill in an empty page, but a link preview scraper or a search crawler may not. A start-mode project renders in one of three ways: a static shell that the browser fills in, a server that streams HTML for each request, or a build step that renders every page to HTML ahead of time. The application code is the same in all three. `src/App.tsx`, `src/Document.tsx`, and the routes carry over; what changes is one or two lines of `vite.config.ts`, what `vite build` produces, and what the host has to run.