From 11ccea9e5d988cf77c50a75f9def35ac7374ca6b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 08:10:05 +0000 Subject: [PATCH 1/3] docs(guides): voice pass to cut generated-sounding prose Rewrite the openers of all 19 guides to say what the page is for instead of staging a scene, drop 'This guide goes through...' signposts and announced counts, split the densest option catalogs (Performance tracks, Observability, Debugging reactivity), remove size-suite numbers, and use common contractions in prose (not headings, so anchors stay stable). WRITING.md: allow common contractions, discourage scene-setting openers, and list the patterns this pass removed under 'Patterns that read as generated'. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWKzrkGqR2qnNqNfyqo --- WRITING.md | 12 ++- src/routes/(5)guides/(0)thinking-in-solid.mdx | 13 ++- .../(1)avoid-unnecessary-effects.mdx | 23 +++-- .../(5)guides/(10)debugging-reactivity.mdx | 76 +++++++-------- src/routes/(5)guides/(11)performance.mdx | 95 ++++++++++--------- src/routes/(5)guides/(12)typescript.mdx | 12 +-- .../(13)integrate-non-solid-code.mdx | 28 +++--- src/routes/(5)guides/(14)testing.mdx | 31 +++--- src/routes/(5)guides/(15)observability.mdx | 78 +++++++-------- .../(5)guides/(16)observability-adapters.mdx | 26 ++--- src/routes/(5)guides/(17)ai-tools.mdx | 11 +-- src/routes/(5)guides/(18)ai-sdk-chat.mdx | 21 ++-- src/routes/(5)guides/(2)custom-primitives.mdx | 23 ++--- src/routes/(5)guides/(3)state-management.mdx | 17 ++-- src/routes/(5)guides/(4)forms.mdx | 40 ++++---- src/routes/(5)guides/(5)lists.mdx | 29 +++--- .../(5)guides/(6)data-fetching-patterns.mdx | 51 +++++----- src/routes/(5)guides/(7)protected-routes.mdx | 36 ++++--- src/routes/(5)guides/(8)ssr-safe-code.mdx | 50 +++++----- .../(5)guides/(9)choose-a-rendering-mode.mdx | 12 +-- 20 files changed, 331 insertions(+), 353 deletions(-) diff --git a/WRITING.md b/WRITING.md index c63cc17b3..5381c5c92 100644 --- a/WRITING.md +++ b/WRITING.md @@ -279,7 +279,9 @@ Every Learn page must do the following. ### Open with something the reader has, wants, or sees -The first paragraph names a concrete situation: a value that does not update, a form that must work before JavaScript loads, a page that flashes a spinner on every navigation. +The first paragraph says what the page is for in terms of a concrete situation: a value that does not update, a form that must work before JavaScript loads, a page that flashes a spinner on every navigation. +Get there in one or two sentences. +Do not stage a scene ("The checkout form throws in production. The fallback renders, the shopper reloads...") before saying what the page covers, and do not follow it with a signpost ("This guide goes through X, shows Y, and ends with Z"). Do not open with a definition ("A signal is a reactive primitive that..."). Definitions belong after the reader knows why they need one. @@ -472,7 +474,9 @@ In cases where jargon is necessary, be sure the document contains at least one b For example, if you want to use an acronym, spell it out first with the acronym in parentheses after. From then on, you can use the acronym in that document. -Plain and simple English also means avoiding contractions, as these are not always easily understood by those for whom English is not the first language. +Common contractions ("don't", "isn't", "can't", "it's") are fine and keep the prose from sounding stiff. +Avoid rarer ones ("it'd", "should've", "y'all") that a reader whose first language is not English may not recognize. +Headings are the exception: write them without contractions, since their anchors are linked from other pages. We should also avoid using expressions or figures of speech wherever possible; remember, not everyone will understand what they mean. Secondly, inclusive language means remaining as gender-neutral as possible. @@ -578,6 +582,10 @@ Avoid the sentence shapes that mark text as machine-written: - Three-item lists used as rhythm rather than content. - A bold lead-in phrase on every list item when the items do not need labels. - Restating the heading as the first sentence under the heading. +- Announcing a count before a list ("Three things this code does not do:", "Two facts matter here:"). Say the things. +- A short dramatic setup line before the explanation ("Nothing is broken:", "None of these know about signals.", "Each alone is not enough."). +- Packing every case, option, and default into one sentence joined by semicolons. Split it, make it a list or table, or leave the option catalog to the reference page. +- Project internals the reader cannot use: size-suite caps, release mechanics, notes addressed to an assistant. ## Solid Terms diff --git a/src/routes/(5)guides/(0)thinking-in-solid.mdx b/src/routes/(5)guides/(0)thinking-in-solid.mdx index d0da94b3a..10ac19133 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've 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 don't 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 @@ -242,10 +241,10 @@ export default function Search() { Type `mug`. Three requests start, one per keystroke; the list shows the answer to `mug` and nothing else, and while each answer is on its way the previous list stays on screen with the `stale` class. -The first time the memo returns a promise, the `Loading` boundary shows **Searching…** in place of the list; after that first answer, the fallback does not come back. +The first time the memo returns a promise, the `Loading` boundary shows **Searching…** in place of the list; after that first answer, the fallback doesn't come back. The memo returned a promise, and that is the whole change. -Readers of `results()` see a `Product[]`, not a `Promise` and not `undefined`; an expression that reads a value that is not ready yet waits, and the nearest `Loading` boundary decides what renders in the meantime. +Readers of `results()` see a `Product[]`, not a `Promise` and not `undefined`; an expression that reads a value that isn't ready yet waits, and the nearest `Loading` boundary decides what renders in the meantime. An answer to a superseded run is dropped, so there is no request counter and no `AbortController`. [Async reactivity](/concepts/async-reactivity) covers what happens between the keystroke and the answer, including `latest` for a controlled input whose `value` should show the new text while the old list is held. @@ -367,7 +366,7 @@ Keep `props` whole and read `props.product.name` inside the JSX; [Props](/concep ### `props.query is not a function` The parent passed `query={query()}`, so the child receives a string and reads it as `props.query`. -A dynamic prop compiles to a getter that runs the parent's expression when the child reads it, so the child does not call it. +A dynamic prop compiles to a getter that runs the parent's expression when the child reads it, so the child doesn't call it. Pass the accessor itself, `query={query}`, when the child should receive a function, and then call `props.query()`. ### The page shows `() => ` or `function` where a value should be diff --git a/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx b/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx index cbf6be77a..7ce25ef50 100644 --- a/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx +++ b/src/routes/(5)guides/(1)avoid-unnecessary-effects.mdx @@ -4,22 +4,21 @@ 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: +An effect belongs at the end of that graph, where a settled result has to leave Solid and drive something Solid doesn't 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 -A value that can be calculated from existing reactive state does not need a signal of its own: +A value that can be calculated from existing reactive state doesn't need a signal of its own: ```tsx import { createEffect, createSignal, createStore } from "solid-js"; @@ -50,7 +49,7 @@ The names come from the `name` option; an unnamed signal prints as `signal`. [Debugging reactivity](/guides/debugging-reactivity#an-effect-writes-a-signal-that-another-scope-derives-from) covers the report and its variants. The `Prefer` version is a plain function. -It runs in the tracking scope that reads it, so it cannot be stale independently of its inputs and it schedules no second write. +It runs in the tracking scope that reads it, so it can't be stale independently of its inputs and it schedules no second write. Keep the call inside JSX or another tracking scope; a component body runs untracked, so `subtotal()` at the top level of a component is a one-time value. Use [`createMemo`](/reference/solid-js/reactivity/create-memo) when several readers share the result or when the derivation should stop propagation when its result is unchanged. @@ -60,7 +59,7 @@ Keep memo computations free of side effects. :::deep-dive[Why the copy is one flush behind] Solid applies writes in a batch. Within one batch it runs every tracked compute function that depends on the changed values, and only then runs the effect functions whose inputs changed. -A write made inside an effect function cannot join the pass that is finishing; it starts a second pass inside the same flush. +A write made inside an effect function can't join the pass that is finishing; it starts a second pass inside the same flush. Readers of the copied signal therefore update one pass after readers of the source, and every effect and DOM write in the first pass saw the stale copy. A memo is a compute function, so it lands in the same pass as its inputs and there is no moment in which the two disagree. ::: @@ -265,7 +264,7 @@ In the `Prefer` version each click runs the [`action`](/reference/solid-js/lifec ## Use an effect at an imperative boundary -Use an effect when a settled reactive value must drive a system that Solid does not own: a third-party widget, a subscription, telemetry, or a browser API with no declarative JSX form. +Use an effect when a settled reactive value must drive a system that Solid doesn't own: a third-party widget, a subscription, telemetry, or a browser API with no declarative JSX form. The two-phase [`createEffect`](/reference/solid-js/reactivity/create-effect) keeps that boundary explicit. The first function tracks and returns a value; the second receives that value, does the imperative work, and may return a cleanup. [Integrate non-Solid code](/guides/integrate-non-solid-code#create-the-instance-once-update-it-in-an-effect) applies this shape to a charting library. @@ -301,7 +300,7 @@ In the `Prefer` version either change re-runs the compute function, the cleanup Solid runs the tracked compute functions for an update before it runs any effect functions. If a compute function reaches pending async work, its effect function waits for that work to settle, so the widget never receives a value from a half-applied update. -:::caution[An effect function is not a place to write reactive state] +:::caution[An effect function isn't a place to write reactive state] The effect function runs after the batch has landed. A setter called there starts a second pass, which is the copy pattern this guide opened with. If the write is needed, ask whether the value could be a memo instead; if it records something the browser produced, see the next section. @@ -353,7 +352,7 @@ function ProductGallery() { Run the `Avoid` version and resize the window. The count never changes, because nothing measures again. -In the `Prefer` version the observer fires on each size change and the write records what the browser reported; it does not copy a reactive value back into the graph. +In the `Prefer` version the observer fires on each size change and the write records what the browser reported; it doesn't copy a reactive value back into the graph. `onSettled` runs its callback once, after the surrounding render has settled, and the returned cleanup runs when the owner is disposed, so the observer is disconnected with the component. [Refs and directives](/concepts/components-and-jsx#refs-and-directives) covers the owned DOM setup this builds on. diff --git a/src/routes/(5)guides/(10)debugging-reactivity.mdx b/src/routes/(5)guides/(10)debugging-reactivity.mdx index cd787877b..cbabd1d9d 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 doesn't 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 ``, 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 ``, and computations by their `name` option, or `effect`/`computed` if they don't 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're 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's 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 isn't sure enough to interrupt you. + +Don't silence a code you don't understand. Each one points at a real bug or a real cost. ## Something does not update @@ -71,7 +72,7 @@ Destructured props produce the same warning and the same fix. ### Is the signal being called? A signal is a function. -`{quantity}` in JSX is a type error, because a function is not a valid DOM child; `"Qty: " + quantity` type-checks but renders the text of the function. +`{quantity}` in JSX is a type error, because a function isn't a valid DOM child; `"Qty: " + quantity` type-checks but renders the text of the function. Search the component for the signal's name without `()` behind it. ### Did the write reach a reactive value? @@ -91,7 +92,7 @@ setCart((draft) => { }); ``` -Run the `Avoid` version and nothing happens: no error, no warning, and the list does not change. +Run the `Avoid` version and nothing happens: no error, no warning, and the list doesn't change. Objects held in a plain signal have the opposite failure. `items().push(item)` does mutate the array, but no one is notified because the signal's value is the same array; `setItems((current) => [...current, item])` gives the signal a new value to report. @@ -100,7 +101,7 @@ The [Stores](/concepts/stores) page covers nested updates. ### Is the effect reading in the wrong phase? `createEffect` tracks its first function only. -A signal read in the second function does not re-run the effect: +A signal read in the second function doesn't re-run the effect: ```ts // Avoid: price() is read in the untracked effect function @@ -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,18 +155,17 @@ 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 didn't 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. Writes made from an effect or an action are stamped with that effect or action; writes from a timer or module scope read as `external`. While attribution is enabled, Solid also warns about the patterns below on its own. -Each warning names the scope and the cause, so you usually do not need to query `why` for them. +Each warning names the scope and the cause, so you usually don't need to query `why` for them. :::tip[Name the scopes before you record] Every report and every `why` chain refers to nodes by their `name` option, and an unnamed memo prints as `computed`, an unnamed effect as `effect`, and an unnamed signal as `signal`. @@ -184,9 +184,9 @@ Read the properties the calculation needs, inside the memo, and nothing else. A plain derived function recomputes for every reader every time an input changes, and passes the recompute downstream even when its result is the same. Turn the link into a `createMemo` when its result is often unchanged, when several readers share it, or when the work downstream is expensive. -The memo compares its result to the previous one and does not notify readers when the two are equal. +The memo compares its result to the previous one and doesn't notify readers when the two are equal. -Do not memoize everything. +Don't memoize everything. A memo costs a node in the graph and a comparison on every run; for a cheap expression with one reader, a function is smaller and faster. ### A memo returns a new object that is equal to the last one @@ -270,11 +270,11 @@ The repair is always to add feedback, never to remove the hold: [Async reactivity](/concepts/async-reactivity#another-answer-is-coming-ispending) explains each of these. -A `Loading` boundary that has not shown content yet, or whose `on` dependencies changed, is a different situation: the read shows the fallback instead of entering a hold, so there is no `SILENT_HOLD` to report. +A `Loading` boundary that hasn't shown content yet, or whose `on` dependencies changed, is a different situation: the read shows the fallback instead of entering a hold, so there is no `SILENT_HOLD` to report. 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 don't 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. @@ -287,7 +287,7 @@ Acknowledged holds under those thresholds still count in the feedback tables: `f A router can name the holds its navigations cause by the route pattern that matched, `/products/:id` rather than `/products/mug`, so occurrences fold together in these tables. It does so by wrapping its location write in `OBSERVE.attribution.withOrigin({ kind: "navigation", name, to, params }, write)`, which is router-agnostic; nothing else in attribution knows about routing. -The [attribution reference](/reference/solid-js/advanced/diagnostics-dev-hooks/attribution#navigationref) describes the `NavigationRef` fields, including how a router whose match is not final at write time fills them in later, how it declares the route the document arrived on with `initial`, and how one that awaits before writing hands the click back as `interaction`. +The [attribution reference](/reference/solid-js/advanced/diagnostics-dev-hooks/attribution#navigationref) describes the `NavigationRef` fields, including how a router whose match isn't final at write time fills them in later, how it declares the route the document arrived on with `initial`, and how one that awaits before writing hands the click back as `interaction`. ::: ## The test sees the old DOM @@ -302,17 +302,17 @@ expect(button).toHaveTextContent("Clicks: 1"); ``` Import `flush` from `solid-js`. -Application code does not normally call it; tests and imperative integrations do. +Application code doesn't normally call it; tests and imperative integrations do. The [Testing](/guides/testing) guide covers the rest of the test setup. -`flush()` drains queued writes; it does not wait for async work. +`flush()` drains queued writes; it doesn't wait for async work. To await one reactive expression from outside a tracking scope, such as an async memo in a test, use [`resolve(fn)`](/reference/solid-js/advanced/interop-async/resolve), which resolves with the first settled value or rejects with the expression's error: ```ts const product = await resolve(() => productMemo()); ``` -One place `flush()` does not belong is inside an action body: +One place `flush()` doesn't belong is inside an action body: ```text [FLUSH_IN_ACTION] flush() inside an action body is not allowed. An action's writes are held in its @@ -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. @@ -392,7 +392,7 @@ test("adding an item recomputes the total once", async () => { ``` `toHaveNoDiagnostics` fails the test when any coded warning fires during the scenario, which turns an untracked read or a leaked effect into a red test instead of a console line nobody reads. -`info` findings do not fail it. +`info` findings don't fail it. The re-run budget and waste checks catch the second kind of bug: a change that makes a scope recompute more than it did before. `toHaveNoSilentHolds` is the responsiveness gate: it fails when a write was held on async work and nothing on screen acknowledged the wait, and its message names the interaction, the held write, and the source it waited on. `toStayWithinHoldBudget(ms)` bounds every hold, acknowledged or not. @@ -406,11 +406,11 @@ Set [`diagnostics: false`](/reference/vite-plugin-solid/options#diagnostics) in ## Recap - Read the bracketed code first; it is stable across releases and names the defect, and the `in` line names the scope that produced it. -- For a value that does not update, check in order: is the read in a tracking scope, is the signal called, did the write go through the setter, is the read in the effect's compute function, is the value pending. +- For a value that doesn't update, check in order: is the read in a tracking scope, is the signal called, did the write go through the setter, is the read in the effect's compute function, is the value pending. - For a value that updates too often, name the scopes, enable attribution, reproduce, and ask `why(scope)`. - Change the one property on a store draft; a spread copy or a fresh array re-runs every reader of the path. - Replace an effect that copies state with a memo; keep the effect only when the write records something from outside the graph. -- When a click looks dead, add feedback with `isPending`, `latest`, an optimistic value, or `affects`; do not remove the hold. +- When a click looks dead, add feedback with `isPending`, `latest`, an optimistic value, or `affects`; don't remove the hold. - Call `flush()` in tests after firing an event, never inside an action body. - Put an `Errored` boundary around any subtree that can throw; an uncaught error halts every update. diff --git a/src/routes/(5)guides/(11)performance.mdx b/src/routes/(5)guides/(11)performance.mdx index a0a782921..dde83dc9a 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 don't re-render, so a slow page is rarely a component running too often. +It's 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's 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 didn't 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 — `, 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, ` › 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 ` › › 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 › · 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 ` › 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 don't 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 don't go looking for a component that runs too often. +If a component function shows up in a profile, you're 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 | | ------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------- | @@ -120,12 +125,12 @@ const visible = createMemo( ``` Run the `Avoid` version and type one character: the filter runs twice, once for the count and once for the list, because a plain function is re-evaluated by each tracking scope that calls it. -In `costs()`, `visible` does not appear at all, since a plain function is not a scope; its time is charged to the scopes that called it. +In `costs()`, `visible` doesn't appear at all, since a plain function isn't a scope; its time is charged to the scopes that called it. In the `Prefer` version the memo runs once, both readers receive the stored array, and `costs()` shows one `visible` row. A memo costs a node in the graph and a comparison on each run; for a cheap expression with one reader, the function is smaller. Reach for the memo when several readers share the result, when the computation is expensive, or when the result is often unchanged. -That last case is the equality gate: a memo compares its new value to the previous one and does not notify readers when the two are equal, so nothing downstream runs. +That last case is the equality gate: a memo compares its new value to the previous one and doesn't notify readers when the two are equal, so nothing downstream runs. The gate compares by reference, which a filter defeats: every run produces a new array. Move the equality boundary to where the value is a primitive: @@ -142,12 +147,12 @@ const visible = createMemo( ); ``` -Type a trailing space and `needle` recomputes, produces the same string, and stops there; `visible` and the 2,000-row list do not run. +Type a trailing space and `needle` recomputes, produces the same string, and stops there; `visible` and the 2,000-row list don't run. When a memo must return an object or array that is often equal in content, pass an `equals` option that compares by content; without one, attribution reports `[UNSTABLE_MEMO_OUTPUT]` after four consecutive new-but-equivalent results, because every subscriber re-ran for nothing. ## 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 @@ -170,12 +175,12 @@ A list is where one mistake is paid once per row. Run the `Avoid` version and add one item to the cart: all 2,000 rows are disposed and recreated, and attribution reports `[UNSTABLE_LIST_IDENTITY]`. In the `Prefer` version `inCart` is a store keyed by product id, built in [Stores at scale](#stores-at-scale), so adding an item updates the one row whose key flipped. -The [Lists](/guides/lists) guide owns the rest: [keeping identity across a refetch](/guides/lists#keep-row-identity-across-updates), [selection without touching every row](/guides/lists#select-a-row), and [rendering a window](/guides/lists#render-a-window-over-a-large-list) with `Repeat` when 2,000 rows should not all be in the DOM. +The [Lists](/guides/lists) guide owns the rest: [keeping identity across a refetch](/guides/lists#keep-row-identity-across-updates), [selection without touching every row](/guides/lists#select-a-row), and [rendering a window](/guides/lists#render-a-window-over-a-large-list) with `Repeat` when 2,000 rows shouldn't all be in the DOM. ## Waterfalls The first paint waits on three requests because each one starts when the one before it lands: the product needs nothing, the brand read waits for the product, and the brand's catalog waits for the brand. -Derive each request from the input you already have, so the ones that do not depend on each other start together, and join the ones that do on the server. +Derive each request from the input you already have, so the ones that don't depend on each other start together, and join the ones that do on the server. With attribution on, three or more sequential flights of 50ms or more print `[ASYNC_WATERFALL]` with the serialized time; a two-flight chain is recorded at `info` only. [Dependent requests](/guides/data-fetching-patterns#dependent-requests) shows the `Avoid` and `Prefer` versions, and a route [`preload`](/routing/solid-router/data#start-work-before-the-component-runs) starts the reads before the component runs at all. @@ -196,7 +201,7 @@ const Account = lazy(() => import("./account/Account")); Render `` for the first time and the dynamic import starts; while it is in flight the component suspends through the nearest `Loading` boundary, which shows the skeleton. Call `Account.preload()` to start the import early, for example from a link's `onMouseEnter`. -For a module that is not a component, such as a route table or a formatting library, [`lazyModule`](/reference/solid-js/components-context/lazy) is the same cached import with `.preload()`, and the server can preload it before hydration. +For a module that isn't a component, such as a route table or a formatting library, [`lazyModule`](/reference/solid-js/components-context/lazy) is the same cached import with `.preload()`, and the server can preload it before hydration. With Solid Router the split usually happens at the route. A route whose `component` is a `lazy` component has its chunk loaded on first navigation, and a `children: () => import("./account/routes")` thunk defers a whole route table the same way. @@ -205,7 +210,7 @@ Link preloading is opt-in: create the router with `createRouter({ routes, preloa :::caution[A chunk is a round trip] Splitting a component that the first page needs adds a request before that page can paint. -Split at the boundary between what the first page shows and what it does not, and leave the header, the product grid, and the cart badge in the main bundle. +Split at the boundary between what the first page shows and what it doesn't, and leave the header, the product grid, and the cart badge in the main bundle. ::: ## Server rendering and streaming @@ -237,7 +242,7 @@ When one read must be in the first HTML, for a crawler or a link preview, pass ` ## Stores at scale -A store holding 2,000 products does not create 2,000 sets of tracking nodes. +A store holding 2,000 products doesn't create 2,000 sets of tracking nodes. The proxy creates a node for a property the first time a tracking scope reads it, so the rows on screen cost nodes and the rest cost nothing until they scroll into view. Two store features keep that cheap as the data changes: @@ -260,7 +265,7 @@ const inCart = createProjection>( ); ``` -Refetch the catalog after a price change and one text node updates; the other 1,999 rows read properties whose values did not change and are not notified. +Refetch the catalog after a price change and one text node updates; the other 1,999 rows read properties whose values didn't change and aren't notified. Add an item to the cart and `inCart` gains one key, so one row's `inCart` read runs. The alternative, every row reading `cart.items.some((item) => item.id === product.id)`, subscribes 2,000 scopes to `cart.items`, and adding an item re-runs all of them. @@ -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. @@ -306,7 +311,7 @@ Measure timings on a production build; use the development build to find out whi - Hand `For` the store's objects; a `map` that produces new objects rebuilds every row. - Derive independent requests from inputs you already have, so they start together. - Split code with `lazy` at the boundary between the first page and the rest, and turn on `preloadLinks: intentPreload()` so the router preloads chunks on hover. -- Wrap each streamed region in a `Loading` boundary so the shell does not wait for its data. +- Wrap each streamed region in a `Loading` boundary so the shell doesn't wait for its data. - Keep per-row flags in a store or projection keyed by id, so a change touches the rows whose key flipped. ## Next steps diff --git a/src/routes/(5)guides/(12)typescript.mdx b/src/routes/(5)guides/(12)typescript.mdx index 7c426d95c..0b8658794 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 @@ -313,7 +309,7 @@ Two details decide whether the declaration applies: TypeScript treats `date-picker.d.ts` beside `date-picker.ts` as that file's compiled output and leaves it out of the program, so the declaration never applies and nothing reports why. Augment `@solidjs/web`, not `solid-js`. -`solid-js` has no `JSX` namespace, so a `declare module "solid-js"` block does not change what the checker accepts in JSX. +`solid-js` has no `JSX` namespace, so a `declare module "solid-js"` block doesn't change what the checker accepts in JSX. [Web components](/guides/integrate-non-solid-code#web-components) covers how attributes, `prop:` properties, and events reach the element at runtime. ## Server functions and the router @@ -408,7 +404,7 @@ Remove the child, or change the props to `ParentProps` if the component should r ### `Type 'SourceAccessor' is not assignable to type 'Element'` A signal was placed in JSX without calling it: `{quantity}` instead of `{quantity()}`. -A function is not a valid child, so the checker reports it; add the parentheses. +A function isn't a valid child, so the checker reports it; add the parentheses. ### `'input' is possibly 'undefined'` 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..6578a087d 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 `` 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 `` 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 don't 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 `` 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 don't 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 @@ -67,11 +65,11 @@ function OrderTotalsChart(props: { totals: number[] }) { } ``` -Run the `Avoid` version and the chart draws the first totals and never changes, because `onSettled` runs its callback once and does not track the reads inside it. +Run the `Avoid` version and the chart draws the first totals and never changes, because `onSettled` runs its callback once and doesn't track the reads inside it. In the `Prefer` version a new `totals` array re-runs the compute function, and the effect function hands the value to the library after the update has landed. The split follows the rest of the documentation: [`onSettled`](/reference/solid-js/lifecycle-actions/on-settled) for one-time setup that returns its teardown, and the effect function of [`createEffect`](/reference/solid-js/reactivity/create-effect) for each settled value that has to leave Solid. -Neither runs during server rendering, so the component is safe to render on the server as long as `chart-lib` can be imported there; if it cannot, see [Keep it off the server](#keep-it-off-the-server). +Neither runs during server rendering, so the component is safe to render on the server as long as `chart-lib` can be imported there; if it can't, see [Keep it off the server](#keep-it-off-the-server). [Use an effect at an imperative boundary](/guides/avoid-unnecessary-effects#use-an-effect-at-an-imperative-boundary) explains why every input belongs in the compute function. The analytics script is the same shape without a DOM node: @@ -123,10 +121,10 @@ The `prop:` namespace assigns a property instead, which is what a custom element The server emits attributes into the HTML and skips `prop:` bindings; the property is assigned when the browser renders or hydrates the element. `onDatechange` lowercases the name and adds a listener for `datechange` with `addEventListener`. -An event name that contains uppercase letters or a hyphen cannot be written that way; use a directive that calls `addEventListener` with the exact name, following the `listen` factory in [Refs and directives](/concepts/components-and-jsx#refs-and-directives). +An event name that contains uppercase letters or a hyphen can't be written that way; use a directive that calls `addEventListener` with the exact name, following the `listen` factory in [Refs and directives](/concepts/components-and-jsx#refs-and-directives). The events Solid delegates, such as `onInput` and `onClick`, are handled by a listener on the render root, so a custom element that dispatches one of those names must dispatch it with `bubbles: true` for the handler to run. -TypeScript does not know ``. +TypeScript doesn't know ``. Declare it once, with the attribute, property, and event names the element supports: ```ts @@ -151,7 +149,7 @@ The `export {}` line keeps the file a module, so the block adds to the types of ## Render Solid into a foreign container -Two situations move Solid content into a DOM node Solid did not create. +Two situations move Solid content into a DOM node Solid didn't create. The first is a subtree that must escape its ancestor, such as a confirmation dialog inside the map library's container, which clips overflow. [`Portal`](/reference/solid-web/components/portal) renders its children into another element, `document.body` by default, while they stay in the component's reactive scope: @@ -242,7 +240,7 @@ function StoreLocator() { } ``` -The server renders the fallback and does not run the import. +The server renders the fallback and doesn't run the import. The browser hydrates the fallback, waits for the module and for hydration to settle, and swaps in `StoreMap` with the props it was given. By default the import starts as soon as `clientOnly` is called; pass `{ lazy: true }` to start it at the component's first render. diff --git a/src/routes/(5)guides/(14)testing.mdx b/src/routes/(5)guides/(14)testing.mdx index 702f9ecc3..84c54219d 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'll 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're testing: 1. Start component tests in jsdom. The `basic` template uses this configuration for component DOM tests. @@ -20,7 +19,7 @@ Beyond that call, testing a Solid app is a question of picking the smallest envi :::tip[Assert what the user sees] Query by an accessible role and assert visible text or state. -Do not inspect signal values, effect counts, or compiled output when the public behavior gives the same evidence. +Don't inspect signal values, effect counts, or compiled output when the public behavior gives the same evidence. This follows the [Testing Library guiding principles](https://testing-library.com/docs/guiding-principles/). ::: @@ -135,7 +134,7 @@ expect(button).toHaveTextContent("Clicks: 1"); The click event stages the signal update, and ordinary reads continue to return the last committed value until the batch lands. [`flush()`](/reference/solid-js/reactivity/flush) commits the staged value and drains queued work so the DOM is current when the assertion runs. -It does not wait for async work: for behavior that is asynchronous by contract, such as an async memo, use the asynchronous queries or [`resolve(fn)`](/reference/solid-js/advanced/interop-async/resolve) rather than `flush()`. +It doesn't wait for async work: for behavior that is asynchronous by contract, such as an async memo, use the asynchronous queries or [`resolve(fn)`](/reference/solid-js/advanced/interop-async/resolve) rather than `flush()`. Solid Testing Library tracks mounted containers and exports `cleanup`. It can register cleanup automatically when the test runner exposes a global `afterEach`. @@ -143,7 +142,7 @@ The configuration above sets `globals: false`, so the explicit `afterEach(cleanu ## Test components in a real browser -Choose browser mode when jsdom cannot provide the behavior that the test needs. +Choose browser mode when jsdom can't provide the behavior that the test needs. The maintained browser-mode template uses the Playwright provider with headless Chromium. For a new test setup, install the complete dependency set: @@ -274,7 +273,7 @@ export default defineConfig({ The Node project compiles server code and resolves the server runtime. The fullstack template also inlines and aliases its `@solidjs/web` entries so the request helper and request-event storage use the same server-build instance. -Keep the server include pattern separate from the client pattern so a server test does not run under jsdom. +Keep the server include pattern separate from the client pattern so a server test doesn't run under jsdom. ### Stub the server environment module @@ -300,7 +299,7 @@ export const env: Record = new Proxy( The proxy reads `process.env` when code accesses a property and reproduces the schema's parsed output for the signing-key list. This behavior lets a test set `process.env.SESSION_SECRET` before it imports the session module. The stub only supplies the module contract needed by the template test. -It does not validate key length or run the start mode server boot process, so use a realistic test key that satisfies the production schema. +It doesn't validate key length or run the start mode server boot process, so use a realistic test key that satisfies the production schema. ### Exercise a session across requests @@ -371,14 +370,14 @@ Restore fake timers and mocks, delete changed environment variables, and dispose ### The assertion after `fireEvent` sees the old DOM -The write is staged and the batch has not landed when the next line runs. +The write is staged and the batch hasn't landed when the next line runs. Call `flush()` after the event and before the assertion. -For an async source, `flush()` does not help; await `resolve(() => value())` or use an asynchronous query. +For an async source, `flush()` doesn't help; await `resolve(() => value())` or use an asynchronous query. ### A component test fails with `document is not defined` -The `test` block does not set `environment`, so Vitest runs the file in Node. -`@solidjs/vite-plugin` does not choose a test environment. +The `test` block doesn't set `environment`, so Vitest runs the file in Node. +`@solidjs/vite-plugin` doesn't choose a test environment. Set `environment: "jsdom"` (or `"happy-dom"`) for component tests, or enable browser mode. ### A server test runs under jsdom @@ -400,7 +399,7 @@ Remove the setting, or reset the module-level state in `afterEach`. - Test in the smallest environment that exercises the behavior: jsdom for component DOM, browser mode for layout and browser APIs, a Node project for server code. - Pass a function to `render` so the component has a reactive owner, and query by role. -- Call `flush()` after `fireEvent` and before the assertion; it lands the staged write and does not wait for async work. +- Call `flush()` after `fireEvent` and before the assertion; it lands the staged write and doesn't wait for async work. - Register `afterEach(cleanup)` when `globals` is off. - Keep jsdom installed in browser mode; the plugin resolves it before the browser pool starts. - Give client and server tests separate `include` patterns in separate Vitest projects. @@ -408,6 +407,6 @@ Remove the setting, or reset the module-level state in `afterEach`. ## Next steps -- [Debugging reactivity](/guides/debugging-reactivity#the-test-sees-the-old-dom): `flush()` in full, `resolve` for async values, and the one place `flush()` is not allowed. +- [Debugging reactivity](/guides/debugging-reactivity#the-test-sees-the-old-dom): `flush()` in full, `resolve` for async values, and the one place `flush()` isn't allowed. - [Sessions and auth](/building-apps/sessions-and-auth): the session module the server test above exercises. - [Environment](/building-apps/environment): why server code that reads `virtual:env/server` needs the module reset the last section describes. diff --git a/src/routes/(5)guides/(15)observability.mdx b/src/routes/(5)guides/(15)observability.mdx index e13ce5bfc..c7350ea09 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's 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 ``, 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 ``, `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 doesn't: 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,16 +69,16 @@ configureClientErrors({ ``` Click "Place order" with a broken price and the `Errored` fallback appears; the hook fires once, with `ownerPath` reading ` › › › computed` and `boundaryPath` reading ` › › `. -The two paths answer different questions: where the code broke, and 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 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 doesn't 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`. A root can carry its own hook ahead of the ambient one: `render(App, el, { onError })` and `hydrate(App, el, { onError })` take the same function, and the nearest root wins. ## 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 wouldn't 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,14 +99,14 @@ configureServerErrors({ }); ``` -`kind` and `handling` say which road the failure took: +`kind` and `handling` tell you what happened: | `kind` | `handling` | What happened | | ----------------- | ----------- | ----------------------------------------------------------------------------------------- | | `render` | `fallback` | An `Errored` boundary rendered its fallback. | | `render` | `client` | A `Loading` fragment rejected; the client re-renders that subtree. | | `render` | `failed` | Nothing contained it; the request fails. | -| `render` | `serialize` | A value written to the hydration stream would not serialize. | +| `render` | `serialize` | A value written to the hydration stream wouldn't serialize. | | `server-function` | `thrown` | The function threw. `direct` is `true` for an in-process call during a render. | | `server-function` | `channel` | A rejection escaped through a returned stream or iterable after the head committed. | | `request` | `failed` | Code outside the runtime, such as middleware, passed a failure to `reportRequestFailure`. | @@ -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 `` tags in an HTML shell's head, one per named entry. -A frame stream and a server-function response have no ``, 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 `` tags in the HTML head. +Frame streams and server-function responses have no ``, so they rely on the header. +You don't 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 ` at › › 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,8 +223,8 @@ 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. -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: +When a click feels slow, the attribution engine tells you what it waited on. +It's 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 import { OBSERVE } from "solid-js"; @@ -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. +`attribution.history("interaction")`, `history("hold")`, `history("navigation")`, and `history("rerun")` return the same records from a ring buffer for a console session that didn't 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"; @@ -310,7 +314,7 @@ Install a provider, or send a sampled `traceparent` from the edge. ### My tool's own effects show up as findings Mark the tool's root with `OBSERVE.exclude(getOwner()!)` as it is created. -Writes into the excluded subtree stay excluded wherever they come from; do not route them through `runWithOwner`, which makes them writes in an owned scope. +Writes into the excluded subtree stay excluded wherever they come from; don't route them through `runWithOwner`, which makes them writes in an owned scope. ## Recap diff --git a/src/routes/(5)guides/(16)observability-adapters.mdx b/src/routes/(5)guides/(16)observability-adapters.mdx index 5528c0670..5c562d5b5 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). @@ -27,12 +27,12 @@ This page is for whoever writes the tool. | Your own UI kept out of the data | `OBSERVE.exclude(owner)` | observe | Import from `solid-js` and `@solidjs/web` only. -Both packages resolve to the build the app chose, so your adapter sees the same `OBSERVE` the app does; `@solidjs/signals`, the engine underneath, is not a public surface. +Both packages resolve to the build the app chose, so your adapter sees the same `OBSERVE` the app does; `@solidjs/signals`, the engine underneath, isn't a public surface. ## Errors: once per object, from the hook alone Both hooks are called once per error object, the first time the runtime meets it. -The client hook fires when an error boundary renders its fallback; an uncaught error halts the reactive system and reaches `reportError`, which your global handler already covers, so the hook does not repeat it. +The client hook fires when an error boundary renders its fallback; an uncaught error halts the reactive system and reaches `reportError`, which your global handler already covers, so the hook doesn't repeat it. The server hook fires for every failure the server runtime handles or fails on; `kind` and `handling` on the context say which, and [Observability](/guides/observability#hear-every-failure-the-server-handles) tabulates them. Both contexts carry `ownerPath`, where the error was thrown, and `boundaryPath`, where it was met, as root-first label arrays. @@ -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's responsible for making sure nothing from the original error crosses the wire. If it doesn't offer that, return nothing. ## Records: settled, serializable, delivered inside the runtime @@ -53,14 +53,14 @@ 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. + A listener that throws is reported to the console and doesn't stop the others. - Server records are delivered during the request, so a Node SDK whose active span is the request's parents them without anything passing a parent around. - A client `"call"` and the server `"invocation"` it caused share `id`. An invocation made during a boundary's render pass carries `boundary`, the `id` of that boundary's record. -- A `"request"` record leaves at the send, before the transport's `fetch`, for a call the `"call"` record cannot show yet: one still in flight, or one that never settles. +- A `"request"` record leaves at the send, before the transport's `fetch`, for a call the `"call"` record can't show yet: one still in flight, or one that never settles. The two share one `live` object by identity. - Bodies cost the call a copy, so they are taken only under `subscribe(type, fn, { bodies: true })`; without it `live.request` is absent and `live.response` is the transport's own, already consumed. @@ -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 @@ -178,8 +178,8 @@ The signals and stores created under it stay excluded wherever their writes come ## Test against the observe build Unit tests resolve `solid-js` and `@solidjs/web` the way any bundler does. -A test runner that applies the `development` condition by default gives you the dev build, which is a superset of observe, so a green test there does not prove the adapter runs on observe. -Point the tests at the observe artifacts explicitly, and keep both packages inside one module loader: a copy of `@solidjs/signals` your runner loads natively beside a copy it transforms is two `OBSERVE` objects, and the one your adapter subscribed to is not the one the app writes to. +A test runner that applies the `development` condition by default gives you the dev build, which is a superset of observe, so a green test there doesn't prove the adapter runs on observe. +Point the tests at the observe artifacts explicitly, and keep both packages inside one module loader: a copy of `@solidjs/signals` your runner loads natively beside a copy it transforms is two `OBSERVE` objects, and the one your adapter subscribed to isn't the one the app writes to. ## Recap diff --git a/src/routes/(5)guides/(17)ai-tools.mdx b/src/routes/(5)guides/(17)ai-tools.mdx index a43761de0..240d5d0cf 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 doesn't 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's there and ends with a block to paste into your assistant's instructions file. ## What ships with the packages @@ -61,7 +60,7 @@ Tell the assistant to treat every code as a defect to look up in the guide, not [`DEV`](/reference/solid-js/advanced/diagnostics-dev-hooks/dev) is the development-only hook object exported from `solid-js`; it is `undefined` in a production build. [`OBSERVE`](/reference/solid-js/advanced/diagnostics-dev-hooks/observe) is its counterpart in the observe build, which also carries the records and attribution channels that tooling subscribes to. -An assistant does not call these directly in app code; they are what the diagnostics package and the Vite plugin are built on, and they are listed here so an assistant that finds them in a stack trace knows what they are. +An assistant doesn't call these directly in app code; they are what the diagnostics package and the Vite plugin are built on, and they are listed here so an assistant that finds them in a stack trace knows what they are. ### `@solidjs/diagnostics` in tests diff --git a/src/routes/(5)guides/(18)ai-sdk-chat.mdx b/src/routes/(5)guides/(18)ai-sdk-chat.mdx index 14f575b7e..764930d41 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 don't 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 doesn't 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 extends AbstractChat { } ``` -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. @@ -218,8 +215,8 @@ Passing `request.signal` as `abortSignal` is the same cancellation contract as i ## What you give up -`AbstractChat` reads and writes every message through the adapter, so the adapter is the whole integration, but the chat is not a Solid computation. -It does not participate in a `Loading` boundary, `isPending` does not report its requests, and a navigation during a stream does not hold the frame; `status` is the only progress signal. +`AbstractChat` reads and writes every message through the adapter, so the adapter is the whole integration, but the chat isn't a Solid computation. +It doesn't participate in a `Loading` boundary, `isPending` doesn't report its requests, and a navigation during a stream doesn't hold the frame; `status` is the only progress signal. For a chat that must take part in the page's async coordination, read the stream through a projection as the streaming guide does, and use the AI SDK only on the server. ## Recap diff --git a/src/routes/(5)guides/(2)custom-primitives.mdx b/src/routes/(5)guides/(2)custom-primitives.mdx index 98f1257ca..1a3ca5641 100644 --- a/src/routes/(5)guides/(2)custom-primitives.mdx +++ b/src/routes/(5)guides/(2)custom-primitives.mdx @@ -4,19 +4,14 @@ 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. +A custom primitive is a function that creates signals, memos and effects and returns what a component needs to read, like a debounced value, a media query, or a draft saved to `localStorage`. +There's nothing special about writing one, but it has to follow the same rules as the built-in primitives, and this guide covers them. The [Reactivity](/concepts/reactivity) page explains tracking, effects, and ownership; this page assumes them. ## A primitive is a function that runs inside an owner Start with the debounce. -The search box should not call the server on every keystroke, so the query signal should change only after typing pauses: +The search box shouldn't call the server on every keystroke, so the query signal should change only after typing pauses: ```ts import { createSignal, onCleanup } from "solid-js"; @@ -129,14 +124,14 @@ Resize the window across 60rem and the page switches between the table and the c Leave the page and the `change` listener is removed. [`onSettled`](/reference/solid-js/lifecycle-actions/on-settled) runs its callback once, after the component's first render has settled, and the cleanup the callback returns runs when the owner is disposed. -It is the home for setup that touches the browser, because the callback does not run during server rendering. +It is the home for setup that touches the browser, because the callback doesn't run during server rendering. `matches` starts as `false` on both sides, so the server renders the cards and the browser switches to the table after hydration if the query matches. The [SSR-safe code](/guides/ssr-safe-code) guide covers what to do when that first frame matters. Use [`onCleanup`](/reference/solid-js/advanced/specialized-reactivity/on-cleanup) when there is nothing to set up after render and only something to release, as the debounce did with its timer. It registers a callback on the current owner and nothing else. -:::caution[Return the cleanup from onSettled; do not register one inside it] +:::caution[Return the cleanup from onSettled; don't register one inside it] Inside an `onSettled` callback, `onCleanup` throws `[CLEANUP_IN_FORBIDDEN_SCOPE] Cannot use onCleanup inside createTrackedEffect or onSettled; return a cleanup function instead`. The same callback may not create signals, memos, or effects; development throws `[PRIMITIVE_IN_FORBIDDEN_SCOPE]`. Create primitives in the body of the `createX` function, and use the callback for the imperative setup and its returned teardown. @@ -192,7 +187,7 @@ Change `breakpoint` in the parent and the old listener is removed, a new `MediaQ The setup moved from `onSettled` into [`createEffect`](/reference/solid-js/reactivity/create-effect) because the input can now change. The compute function reads the input, the effect function does the browser work and returns its cleanup, and Solid runs that cleanup before the next effect run and on disposal. -Like `onSettled`, the effect function does not run during server rendering. +Like `onSettled`, the effect function doesn't run during server rendering. [`Accessor`](/reference/solid-js/types/reactive-types) is the type of a signal getter and of any `() => T`, so a caller can pass a signal, a memo, or an arrow function over props. Take a plain `T` only for a value the primitive reads once by design, such as `initial` in `createDebouncedSignal`. @@ -349,7 +344,7 @@ dispose(); ``` Use it in a test of the primitive itself, or in an integration that hosts Solid reactivity in code that has no `render` call. -Application code should not need it; state that several components share belongs in [context](/concepts/components-and-jsx#context), not in a root at module scope. +Application code shouldn't need it; state that several components share belongs in [context](/concepts/components-and-jsx#context), not in a root at module scope. ## Common problems @@ -383,12 +378,12 @@ The [SSR-safe code](/guides/ssr-safe-code) guide has the full checklist. - Accept `MaybeAccessor` and unwrap it inside a tracking scope; a plain parameter is a one-time read. - Return accessors, tuples of accessors, or a store; never the result of calling one. - Return an async memo as you would a sync one; the caller's `Loading` boundary handles the wait. -- Read browser state in `onSettled`, write it in an effect function, and use `defer: true` when the first run should not write. +- Read browser state in `onSettled`, write it in an effect function, and use `defer: true` when the first run shouldn't write. - Capture the owner with `getOwner` and re-enter it with `runWithOwner` when a primitive must create something after an `await`. ## Next steps -- [Avoid unnecessary effects](/guides/avoid-unnecessary-effects): which of the values a primitive returns should be memos, and when its effect is one that should not exist. +- [Avoid unnecessary effects](/guides/avoid-unnecessary-effects): which of the values a primitive returns should be memos, and when its effect is one that shouldn't exist. - [Integrate non-Solid code](/guides/integrate-non-solid-code): the same owner and cleanup rules applied to charting libraries, maps, and web components. - [SSR-safe code](/guides/ssr-safe-code): the checklist for primitives whose first frame differs between server and browser. - [TypeScript](/guides/typescript): typing `Accessor`, `Setter`, and `MaybeAccessor` parameters and return values. diff --git a/src/routes/(5)guides/(3)state-management.mdx b/src/routes/(5)guides/(3)state-management.mdx index 21933e31f..1e6b31fcb 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 @@ -22,7 +19,7 @@ This guide goes through the homes one at a time and ends with a decision list. | On the server | The catalog, orders, the session | Server functions; the client holds a view, not a copy | The first three differ only in where the provider sits. -The last two are not held in Solid state at all: the URL and the server own them, and components read them through a router primitive or a server function. +The last two aren't held in Solid state at all: the URL and the server own them, and components read them through a router primitive or a server function. ## Local state stays local @@ -74,7 +71,7 @@ Props are reactive on their own, so a lifted value costs nothing but the two lin ## Share with context -The header badge and the checkout page are far apart in the tree, and threading the cart through every layout between them would make each layout know about a cart it does not use. +The header badge and the checkout page are far apart in the tree, and threading the cart through every layout between them would make each layout know about a cart it doesn't use. Create the cart once in a provider and let both read it: ```tsx @@ -341,7 +338,7 @@ Ask these in order and stop at the first yes: 5. Do components far apart, or every page, need it? Create it in a provider and read it with `useContext`; place the provider at the root of `App` for app-wide state. -Module scope is not on the list. +Module scope isn't on the list. Use it for constants and for the `createContext` call itself. ## Common problems @@ -378,7 +375,7 @@ Pass the store, the accessor, or a function, and read it in the consumer; see [P - Share state across distant components with a provider that creates the store and a `useContext` read below it; put the provider at the root of `App` for app-wide state. - Pass stores, accessors, and functions through context; the provider reads `value` once. - Use `createContext()` with no default for reactive state so a missing provider throws. -- Do not create signals or stores at module scope in code that runs on the server; the module loads once and one object serves every request. +- Don't create signals or stores at module scope in code that runs on the server; the module loads once and one object serves every request. - Put anything a shared link should reproduce in search parameters with `useSearchParams`. - Read server-owned data through a server function into a memo or store; the client holds a view, and `refresh` re-reads it. diff --git a/src/routes/(5)guides/(4)forms.mdx b/src/routes/(5)guides/(4)forms.mdx index 3cec8ee4d..301f8c9fd 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 a checkout shipping-address form in four steps, and each step is a working form you can stop at: -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. +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 can't 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. @@ -22,7 +18,7 @@ This guide uses `formAction` from `@solidjs/router`. It wraps a function so a form can submit to it and the router can track the submission. `solid-js` exports its own [`action`](/reference/solid-js/lifecycle-actions/action); that one runs a generator as a reactive transaction and is covered in [Mutations](/concepts/mutations). -`
` fails at the type level because the core `action` does not produce a URL. +`` fails at the type level because the core `action` doesn't produce a URL. The router's `formAction` uses the core one internally. ::: @@ -83,7 +79,7 @@ Fill in the three fields and click **Continue to shipping**: the browser posts t :::note[Where the server function file lives] The server function is in `src/data/`, not next to the page. -With method discovery on, a file under `src/routes` becomes an [API route](/building-apps/middleware-and-api-routes#api-routes) as soon as it exports an uppercase HTTP method; keeping server functions in `src/data` means a later export cannot turn one into a route by accident. +With method discovery on, a file under `src/routes` becomes an [API route](/building-apps/middleware-and-api-routes#api-routes) as soon as it exports an uppercase HTTP method; keeping server functions in `src/data` means a later export can't turn one into a route by accident. ::: `formAction(saveAddress)` returns a value that serializes to the server function's URL, so `action={submitAddress}` renders as an ordinary `action="..."` attribute in the HTML. @@ -96,11 +92,11 @@ Both paths run the same function with the same `FormData`. [Add router submissions](/building-apps/server-functions/mutations-and-responses#add-router-submissions) covers the inline shape and when to use it. Two details are doing the work here. -The inputs are uncontrolled: Solid does not read their values while the user types, and the browser assembles the `FormData` on submit. -`required` and `pattern` are the browser's own validation; the browser does not submit until they pass. +The inputs are uncontrolled: Solid doesn't read their values while the user types, and the browser assembles the `FormData` on submit. +`required` and `pattern` are the browser's own validation; the browser doesn't submit until they pass. This is a complete, deployable form. -What it lacks is validation the user cannot bypass and any feedback other than a redirect. +What it lacks is validation the user can't bypass and any feedback other than a redirect. ## Pass 2: validate on the server @@ -145,7 +141,7 @@ export async function saveAddress(form: FormData) { ``` `Object.fromEntries(form)` turns the `FormData` into a plain object for the schema. -Post a bad postal code with `curl` and the response is a `400` whose body carries the field messages; the database is not touched. +Post a bad postal code with `curl` and the response is a `400` whose body carries the field messages; the database isn't touched. The failure leaves the function as a thrown `respond()` envelope, and the choice of `respond()` over a plain error is what makes the messages reach the client: @@ -161,14 +157,14 @@ if (!parsed.success) { } ``` -Run the `Avoid` version in a production build and the client receives `Internal Server Error` with no field messages, because the runtime replaces an unmarked thrown `Error` with a generic message so a stack trace or a database error cannot leak. +Run the `Avoid` version in a production build and the client receives `Internal Server Error` with no field messages, because the runtime replaces an unmarked thrown `Error` with a generic message so a stack trace or a database error can't leak. Throwing an envelope is intentional control flow: the runtime keeps the `400` and the value in development and production alike. [Handle thrown errors](/building-apps/server-functions/mutations-and-responses#handle-thrown-errors) covers `markSafeError` for the cases where the thrown value is an `Error` on purpose. -The schema and `valibot` are used only inside the `"use server"` body, so they do not enter the client bundle. +The schema and `valibot` are used only inside the `"use server"` body, so they don't enter the client bundle. The `AddressIssues` type is exported for the client to use in the next pass. -The component from pass 1 does not change. +The component from pass 1 doesn't change. With JavaScript on, a failed submission is recorded by the router but nothing displays it yet. Without JavaScript, the runtime redirects back to the form and the router records the same submission on the next server render. On its own, pass 2 protects the database; the user sees a form that appears to have done nothing. @@ -244,11 +240,11 @@ The `` element and its inputs are the same as in pass 1; only the message `useSubmissions(submitAddress)` returns a reactive array of this form action's settled submissions, each with the `input` that was sent and either a `result` or an `error`. When `saveAddress` throws the `respond()` envelope, the carried value becomes `submission.error`, which is where `issues()` reads the field messages. -A submission that returned a redirect does not stay in the list. +A submission that returned a redirect doesn't stay in the list. `issues()` is a plain derived function read from JSX, following the rule from the [Reactivity](/concepts/reactivity) page. :::deep-dive[How the error reaches the page without JavaScript] -When the form posts without the client, the server-function runtime cannot hand the `400` to a script. +When the form posts without the client, the server-function runtime can't hand the `400` to a script. It stores the thrown value in a one-shot cookie and redirects back to the form's URL. The router reads that cookie during the next server render and records the same submission it would have recorded from a scripted call, and the cookie is cleared once it has been read. `useSubmissions` sees one entry either way, so the inline errors render on that page load. @@ -364,7 +360,7 @@ Post a bad postal code and the line disappears when the `400` arrives, because a Disable JavaScript and submit the same form. The browser posts, `addAddress` runs, the `reload` sends the browser back to the page, and the server renders the list with the new address in it. The `createOptimisticStore` and the `.onSubmit` hook did nothing on that path, and nothing had to be written to make the form fall back to it. -Each pass in this guide added a layer to the same ``: pass 1 made it work, pass 2 made it safe, pass 3 made it explain itself, and pass 4 made it feel immediate; the shopper whose bundle has not arrived yet still gets pass 1. +Each pass in this guide added a layer to the same ``: pass 1 made it work, pass 2 made it safe, pass 3 made it explain itself, and pass 4 made it feel immediate; the shopper whose bundle hasn't arrived yet still gets pass 1. ## Live validation while typing @@ -439,7 +435,7 @@ Every field that should reach the server needs a `name`, and mutations must post ### The form submits, but the page reloads instead of staying put -The router is not mounted around the form, so nothing intercepts the submit and the browser follows the `action` URL as a full-page navigation. +The router isn't mounted around the form, so nothing intercepts the submit and the browser follows the `action` URL as a full-page navigation. Check that the page is rendered inside the `Router`. ### `action` is not a function, or the form attribute renders as source code diff --git a/src/routes/(5)guides/(5)lists.mdx b/src/routes/(5)guides/(5)lists.mdx index 5237f875c..72d708031 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're 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: @@ -48,7 +43,7 @@ function CartLines(props: { items: CartItem[] }) { ``` Pass three items and three rows render, numbered 1 to 3; pass an empty array and the fallback row shows. -The callback runs once per item that `For` has not seen before, and its JSX is created once. +The callback runs once per item that `For` hasn't seen before, and its JSX is created once. `item` is the item itself. `index` is an accessor, because the same item can move to another position without being recreated; read `index()` where the position is displayed. @@ -60,7 +55,7 @@ Everything else on this page follows from that rule. :::deep-dive[What a rebuilt row loses] A row is a set of DOM nodes plus the reactive scopes that fill them. Disposing one removes the nodes, so focus, text selection, and an in-progress CSS animation go with them, and it disposes the scopes, so any signal the row component created starts over in the new row. -The list itself does not know a row was rebuilt for the same record; `[UNSTABLE_LIST_IDENTITY]` exists because attribution compares the disposed and created items field by field and reports when they match. +The list itself doesn't know a row was rebuilt for the same record; `[UNSTABLE_LIST_IDENTITY]` exists because attribution compares the disposed and created items field by field and reports when they match. ::: ## Keep row identity across updates @@ -131,7 +126,7 @@ function saveForLater(id: string) { Run the `Avoid` version while the cursor is in that row's quantity input and the input loses focus, because the row was torn down and a new one created; every reader of the old item object runs as well. Development builds flag it as `[IMMUTABLE_UPDATE_IN_STORE]` when a path is replaced by a container whose leaves are mostly unchanged. -In the `Prefer` version only the readers of that item's `savedForLater` property run; the row is not recreated, the list is not diffed, and the other rows are untouched. +In the `Prefer` version only the readers of that item's `savedForLater` property run; the row isn't recreated, the list isn't diffed, and the other rows are untouched. Adding and removing follow the same rule: @@ -148,7 +143,7 @@ Use [`reconcile`](/reference/solid-js/stores/reconcile) when the whole list arri ## Filter and sort -Derive the visible list; do not store it: +Derive the visible list; don't store it: ```tsx const [filter, setFilter] = createSignal<"all" | "active" | "saved">("all"); @@ -178,12 +173,12 @@ Switch the sort to price and the rows move into their new order; no row is recre `visible()` is a new array on every change, and that is fine. The items inside it are the same proxies as before, so `For` keeps every row and reorders or hides them. -:::note[A derived array is not a rebuilt list] +:::note[A derived array isn't a rebuilt list] A derived array costs a diff; a new set of objects costs a rebuild. Those are different things, and only the second is a problem. ::: -When the derived list is large and read by several consumers, [`createProjection`](/reference/solid-js/stores/create-projection) produces a store-shaped result instead of an array, so readers of individual rows do not subscribe to the whole list; [Stores](/concepts/stores#derive-a-store-with-a-projection) shows the shape. +When the derived list is large and read by several consumers, [`createProjection`](/reference/solid-js/stores/create-projection) produces a store-shaped result instead of an array, so readers of individual rows don't subscribe to the whole list; [Stores](/concepts/stores#derive-a-store-with-a-projection) shows the shape. ## Select a row @@ -260,7 +255,7 @@ When `from` moves, rows whose index is still in range are kept, rows that left a :::caution[Repeat is positional] A `Repeat` row shows whatever is at its index, so it fits logs, tables, and grids over store data. -It does not fit lists where a row must follow its item as the list reorders; use `For` for those. +It doesn't fit lists where a row must follow its item as the list reorders; use `For` for those. ::: For a full virtualizer with variable heights, use a library; the pattern above is what those libraries build on. @@ -282,7 +277,7 @@ A `Loading` boundary is the not-yet state: ``` The skeleton shows until the first response; the "Your cart is empty" row shows when that response is empty. -A refetch does not bring the skeleton back; the rows stay, and `isPending(() => items.length)` reports the wait if you want to dim them. +A refetch doesn't bring the skeleton back; the rows stay, and `isPending(() => items.length)` reports the wait if you want to dim them. ## Common problems @@ -309,7 +304,7 @@ Write through `setItems((draft) => { draft.push(item); })`. ## Recap - `For` keeps a row when the same object comes back and rebuilds it when a new object appears; every other rule follows from that. -- Load server lists into a store with `createStore(async () => ..., [])`, or pass `keyed={(item) => item.id}`, so a refetch does not rebuild rows. +- Load server lists into a store with `createStore(async () => ..., [])`, or pass `keyed={(item) => item.id}`, so a refetch doesn't rebuild rows. - Edit one property on the setter's draft; a `map` with a spread creates a new object and a new row. - Derive filtered and sorted views with a memo; a new array of the same proxies is a diff, not a rebuild. - Keep selection in a store keyed by id so a change touches two rows, not every row. diff --git a/src/routes/(5)guides/(6)data-fetching-patterns.mdx b/src/routes/(5)guides/(6)data-fetching-patterns.mdx index 69ce39fd6..0729027e9 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. +In other frameworks these usually need 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 @@ -43,7 +43,7 @@ const [orders] = createStore( ); ``` -The response reconciles into the same proxy by `id`, so a row that did not change keeps its DOM. +The response reconciles into the same proxy by `id`, so a row that didn't change keeps its DOM. [Stores](/concepts/stores#fetch-into-a-store) covers when that matters. ## Search as you type @@ -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: +You don't 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. -- 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. +The list doesn't 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. @@ -130,11 +127,11 @@ function ProductPage(props: { id: string }) { ``` Each boundary reveals when its own data is ready, so the header can appear before the reviews. -If the page should not reveal out of order, wrap the boundaries in [`Reveal`](/reference/solid-js/components-jsx/reveal): `order="sequential"` shows them top to bottom, `order="together"` waits for all three. +If the page shouldn't reveal out of order, wrap the boundaries in [`Reveal`](/reference/solid-js/components-jsx/reveal): `order="sequential"` shows them top to bottom, `order="together"` waits for all three. When `props.id` changes, all three requests start again and the update is held until all three have answered; the page then swaps as one. That is usually what you want for a detail page, where the header and the reviews must describe the same product. -If a slow section should not delay the others on a subject change, give it `on={props.id}`: it stops holding the update, so the page swaps as soon as the other two have answered, and the slow section shows its skeleton from that swap until its own data lands. +If a slow section shouldn't delay the others on a subject change, give it `on={props.id}`: it stops holding the update, so the page swaps as soon as the other two have answered, and the slow section shows its skeleton from that swap until its own data lands. Give all three `on` and the page swaps on the click itself, each section on its skeleton until its data arrives. The [Async reactivity](/concepts/async-reactivity#show-a-placeholder-again-loading-on) page explains when the skeleton lands, and why it never can when the same data is also read outside the boundary. @@ -147,10 +144,10 @@ const product = createMemo(() => api.product(props.id)); const brand = createMemo(() => api.brand(product().brandId)); ``` -`brand` reads `product().brandId`, so it cannot start until `product` resolves. +`brand` reads `product().brandId`, so it can't start until `product` resolves. The dependency is visible in the code, and that is the right shape when the data is sequential. -The trap is a chain the data does not need. +The trap is a chain the data doesn't need. A product page that also shows the brand's other products can be written as three steps or as two independent requests: ```tsx @@ -165,7 +162,7 @@ const brand = createMemo(() => api.brand(product().brandId)); const catalog = createMemo(() => api.brandProducts(product().brandId)); ``` -Run the `Avoid` version and the catalog request does not start until the brand has arrived, although `brandId` was known as soon as the product was. +Run the `Avoid` version and the catalog request doesn't start until the brand has arrived, although `brandId` was known as soon as the product was. The page waits for the sum of the three requests instead of the product request plus the longer of the other two. With attribution enabled, and the memos named through their `name` option, development reports a chain of three or more sequential requests: @@ -173,8 +170,8 @@ With attribution enabled, and the memos named through their `name` option, devel [ASYNC_WATERFALL] 3 sequential async flights — "product" (120ms) → "brand" (80ms) → "catalog" (95ms) — 295ms serialized: each began only after the previous resolved (as far as this graph can see). If a later request doesn't need the earlier response, derive both from the same inputs so they start together; if the dependency is intrinsic, preload the dependent data or join the requests server-side. ``` -A chain of two is recorded at `info` severity and does not reach the console, because two steps can be a real data dependency; three or more is a warning. -Requests shorter than 50ms do not count as a step. +A chain of two is recorded at `info` severity and doesn't reach the console, because two steps can be a real data dependency; three or more is a warning. +Requests shorter than 50ms don't count as a step. [Something updates too often](/guides/debugging-reactivity#something-updates-too-often) explains how to enable attribution and read its reports. When the dependency is real, remove it from the client rather than working around it: @@ -274,14 +271,14 @@ Every thirty seconds the numbers update in place; nothing dims and no skeleton s A bare `refresh` is quiet: the current answer still fits the question, so `isPending` stays `false` and the new value replaces the old one without a pending phase. When the reload should be visible, declare it: call [`affects(stats)`](/reference/solid-js/lifecycle-actions/affects) inside an action before the `refresh`, and readers report pending until it lands. -:::deep-dive[Why a refresh is quiet and a changed input is not] +:::deep-dive[Why a refresh is quiet and a changed input isn't] `isPending` answers the question "is a different answer on the way for a changed input?". When `page` goes from 1 to 2, the committed value answers a question that is no longer being asked, so readers of the held update report pending. A `refresh` asks the same question again; the committed value is still a valid answer to it, so nothing is pending and the new value lands as a plain update. `affects(source)` marks the source as pending for as long as the surrounding action is in flight, which is how a refetch inside an action is made visible. ::: -Polling is the fallback when the server cannot push. +Polling is the fallback when the server can't push. When it can, a [`live()` server function](/building-apps/server-functions/reads-and-live-data#declare-a-live-source) returns an async iterable that a memo consumes like any other async source, and each yielded value becomes the next answer. ## Mutate, then refetch @@ -295,7 +292,7 @@ const addReview = action(function* (productId: string, text: string) { }); ``` -The reviews list does not flicker: the refetch runs inside the action, and the list updates once when the fresh data lands. +The reviews list doesn't flicker: the refetch runs inside the action, and the list updates once when the fresh data lands. To show the new review before the server confirms it, hold the list in a `createOptimisticStore` and write to it before the `yield`; [Mutations](/concepts/mutations) walks through that version. With Solid Router mounted, a server function's response revalidates `query` reads automatically, and `formAction` from `@solidjs/router` adds form submissions on top; the [Forms guide](/guides/forms) uses it. @@ -323,7 +320,7 @@ Place boundaries where a failure should be contained: A failed reviews request shows the retry panel and leaves the header alone. `reset` retries the sources the boundary collected, and a boundary also recovers on its own when an input changes or a `refresh` lands. -Throw from the request when the response is not usable, rather than returning a `{ success: false }` object and checking it at every read. +Throw from the request when the response isn't usable, rather than returning a `{ success: false }` object and checking it at every read. On the server, [`markSafeError`](/reference/solid-web/request-response/safe-errors) marks a message that is intended for the client; unmarked errors are replaced with a generic message in production. ## Common problems @@ -356,7 +353,7 @@ Read the input directly so both requests start together; with attribution enable ## Recap - Make each request a memo or a store created where the data is needed, or higher when it should start earlier. -- Create requests that do not depend on each other in the same scope so they run in parallel; derive from inputs you already have rather than from another response. +- Create requests that don't depend on each other in the same scope so they run in parallel; derive from inputs you already have rather than from another response. - Wrap the smallest region each `Loading` fallback should replace, and set `on` where a changed subject should show the fallback again. - Read `latest` in controls whose writes feed a request, and mark waiting content with `isPending`. - Leave stale responses, loading flags, and abort controllers to Solid; returning a synchronous value is a settled answer. diff --git a/src/routes/(5)guides/(7)protected-routes.mdx b/src/routes/(5)guides/(7)protected-routes.mdx index aa76da630..81d4187d9 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. @@ -54,7 +52,7 @@ export const getOrders = query(async () => { ``` Call `getOrders` from an HTTP client at the function's URL without the session cookie and the answer is a redirect with no order data in it. -`locals.userId` was set by the `attachCustomer` middleware in [Authenticate once in middleware](/building-apps/sessions-and-auth#authenticate-once-in-middleware), and the caller cannot supply it, which is why the identity comes from the event rather than from an argument. +`locals.userId` was set by the `attachCustomer` middleware in [Authenticate once in middleware](/building-apps/sessions-and-auth#authenticate-once-in-middleware), and the caller can't supply it, which is why the identity comes from the event rather than from an argument. [Authorize on the server](/building-apps/sessions-and-auth#authorize-on-the-server) covers the check itself; this page adds nothing to it except the rule that it goes in every function the account pages read or write. `getCurrentUser` returns `null` rather than throwing, because a signed-out visitor is a normal state for the header to show. @@ -107,7 +105,7 @@ export default function RequireUser(props: RouteSectionProps) { } ``` -Visit `/account/orders` and the match is the pathless route, then `/account`, then `/orders`; the URLs do not change. +Visit `/account/orders` and the match is the pathless route, then `/account`, then `/orders`; the URLs don't change. `RequireUser` renders no frame of its own, only the matched page once the check has a user. `/sign-in` is a sibling of the group rather than a child of it; a sign-in page inside the guard would redirect to itself. @@ -137,7 +135,7 @@ The preload is what starts the check as soon as the route matches, and when a [l Both call the same function. Its body runs on the server either way: in-process during server rendering, where it shares the request's `getCurrentUser` result with the header, and through the server-function endpoint during a navigation, where the thrown redirect travels back to the router as the response. -With `preloadLinks: intentPreload()` on the router, hovering the **Account** link runs the preload with the `"preload"` intent, which starts the request and does not act on its outcome, so a hover never redirects; the click that follows does, whether it reuses that answer or fetches a new one. +With `preloadLinks: intentPreload()` on the router, hovering the **Account** link runs the preload with the `"preload"` intent, which starts the request and doesn't act on its outcome, so a hover never redirects; the click that follows does, whether it reuses that answer or fetches a new one. :::deep-dive[What the router does with a thrown redirect] `query` treats a value thrown by the wrapped function the same way as a returned one. @@ -218,7 +216,7 @@ Everything else falls back to the account home. :::danger[Validate next on the server, not only in the form] The hidden input is caller-controlled: anyone can post `next=//attacker.example` to `signIn`, whether or not the sign-in page put it there. The server-function transport resolves a redirect target against the request URL, so `//attacker.example` and `/\attacker.example` both resolve to another origin, and the router leaves the app for a target on another origin by setting `window.location.href`. -The runtime refuses targets whose scheme is not `http` or `https`, and it does not refuse a different `http` origin. +The runtime refuses targets whose scheme isn't `http` or `https`, and it doesn't refuse a different `http` origin. Run `safeNext` inside `signIn`, where the value is about to become a `Location` header, and treat the copy in the form as a convenience. ::: @@ -264,12 +262,12 @@ Request `/account/orders` with no cookie and the server answers `302` with `Loca Use this layer in addition to the route guard, not instead of it. What it adds is a real redirect status on full page requests: the route guard's redirect, when it lands after the shell has flushed, reaches the browser as a script at the end of the stream, after the fallback has already painted. -What it cannot do is see a client-side navigation, which makes no page request, or a server-function call, which is a request to `/_server` rather than to `/account/*`. +What it can't do is see a client-side navigation, which makes no page request, or a server-function call, which is a request to `/_server` rather than to `/account/*`. -:::caution[A path prefix is not a permission] +:::caution[A path prefix isn't a permission] The middleware matches URLs, so it protects exactly the paths that start with `/account` and nothing else. The server functions the account pages call are reachable at their own URLs, and the check inside each function is what protects them. -Adding a prefix to the middleware does not remove a check from a function. +Adding a prefix to the middleware doesn't remove a check from a function. ::: ## Show the right navigation @@ -320,7 +318,7 @@ Sign in and the name appears; sign out through a router `formAction`, or a sign- The sign-in route is a child of the pathless guard route, so the guard redirects to a page it guards. Move `/sign-in` beside the group. -If the routes are right, the session cookie is not coming back on the next request; [Sessions and auth](/building-apps/sessions-and-auth#common-problems) lists the cookie attributes to compare. +If the routes are right, the session cookie isn't coming back on the next request; [Sessions and auth](/building-apps/sessions-and-auth#common-problems) lists the cookie attributes to compare. Chained client-side redirects stop after 100 with `Too many redirects` from the router. ### The account page paints, then the sign-in page replaces it @@ -341,12 +339,12 @@ Accept only paths that start with a single `/` and reject `//` and `/\`; run tha ## Recap -- Decide once, on `locals.userId`, and run the decision in every server function, in the route guard, and in middleware; each layer covers a way in that the others do not see. +- Decide once, on `locals.userId`, and run the decision in every server function, in the route guard, and in middleware; each layer covers a way in that the others don't see. - Put the account pages under a pathless route whose `preload` starts the check and whose component renders `props.children` behind it, and keep `/sign-in` beside the group. - Make the guard a server function wrapped in `query` that throws `redirect()` when the session has no user; the router navigates on it during server rendering and on client-side navigations alike. - Build the sign-in target with `paths["sign-in"]({ next })`; the search object encodes the path. - Validate `next` inside the server function: a single leading `/`, no `//`, no `/\`, and not the sign-in page itself. -- Return a redirect from middleware for full page requests under `/account`, and keep the function checks, because middleware does not see `/_server` or client-side navigations. +- Return a redirect from middleware for full page requests under `/account`, and keep the function checks, because middleware doesn't see `/_server` or client-side navigations. - Read `getCurrentUser()` through `query` in the header so the server result hydrates and a sign-out form action refreshes it. ## Next steps diff --git a/src/routes/(5)guides/(8)ssr-safe-code.mdx b/src/routes/(5)guides/(8)ssr-safe-code.mdx index 0aa510bc8..9c0187f06 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 can't 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 @@ -65,7 +63,7 @@ export function CartSummary() { ``` Request the page and the server runs the module once, the component body once per request, the store's function and the memo, and the compute function of the effect. -It does not run the effect function or the `onSettled` callback: on the server `createEffect` runs the compute function once and drops the effect function, and `onSettled` is a no-op that reserves a hydration id. +It doesn't run the effect function or the `onSettled` callback: on the server `createEffect` runs the compute function once and drops the effect function, and `onSettled` is a no-op that reserves a hydration id. The browser runs all of it, and `document.title` changes after hydration. The rule for browser-only work follows from that list: put it in an effect function or `onSettled`. @@ -123,7 +121,7 @@ Resizing the window updates it again through the listener, and the returned clea [Custom primitives](/guides/custom-primitives#clean-up-what-you-start) turns this into a reusable `createMediaQuery`. The `false` seed is right while the browser is claiming server HTML, and wrong when the drawer is created later by a client-side navigation: there is no server markup to agree with, and the class still flips one frame after it appears. -[`isHydrating`](/reference/solid-js/advanced/manual-hydration/is-hydrating) tells the two apart, so the seed can ask the browser whenever the answer will not be checked against the server: +[`isHydrating`](/reference/solid-js/advanced/manual-hydration/is-hydrating) tells the two apart, so the seed can ask the browser whenever the answer won't be checked against the server: ```tsx import { createSignal, isHydrating, onSettled } from "solid-js"; @@ -136,11 +134,11 @@ const [compact, setCompact] = createSignal( ); ``` -`isHydrating()` is `true` during the root pass of `hydrate()` and while a streamed boundary resumes, and `false` on the server, in client-only renders, and after hydration; it is not reactive, so read it where the signal is created. +`isHydrating()` is `true` during the root pass of `hydrate()` and while a streamed boundary resumes, and `false` on the server, in client-only renders, and after hydration; it isn't reactive, so read it where the signal is created. The `onSettled` callback stays: it is still what updates the value after a hydrated first paint. Some helpers are called from places that have no owner, such as a formatting function shared with server code. -For those, [`isServer`](/reference/solid-web/rendering-ssr/is-server) from `@solidjs/web` is a build-time constant, `true` in the server build and `false` in the browser build, so the bundler drops the branch that cannot run: +For those, [`isServer`](/reference/solid-web/rendering-ssr/is-server) from `@solidjs/web` is a build-time constant, `true` in the server build and `false` in the browser build, so the bundler drops the branch that can't run: ```ts import { isServer } from "@solidjs/web"; @@ -151,10 +149,10 @@ export function prefersReducedMotion() { } ``` -`isServer` stops the crash; it does not make the two renders agree. +`isServer` stops the crash; it doesn't make the two renders agree. A helper like this returns `false` on the server and possibly `true` in the browser, so use its result in an effect function or `onSettled`, not in JSX that decides which elements exist. -For a component that cannot be made safe, such as a map or a rich text editor whose library touches `window` at import, [`clientOnly`](/reference/solid-web/rendering-ssr/client-only) renders a fallback on the server and swaps the component in after hydration. +For a component that can't be made safe, such as a map or a rich text editor whose library touches `window` at import, [`clientOnly`](/reference/solid-web/rendering-ssr/client-only) renders a fallback on the server and swaps the component in after hydration. [Keep it off the server](/guides/integrate-non-solid-code#keep-it-off-the-server) covers it, including the `{ lazy: true }` option. ## Values that differ on every run @@ -177,14 +175,14 @@ import { Show } from "solid-js"; ``` Run the `Avoid` version and half of the page loads disagree. -When the server rendered the banner and the browser did not, development warns after hydration with `Hydration completed with 1 unclaimed server-rendered node(s):` followed by the banner's HTML, and the server's banner stays in the document with nothing attached to it. -When the browser rendered it and the server did not, development warns `Hydration key miss for "..."`: the browser created the `