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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/routes/(0)index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Read them in this order the first time through:
3. [Reactivity](/concepts/reactivity): what a signal is, why reads have to happen in the right place, and when updates land.
4. [Components and JSX](/concepts/components-and-jsx): props, events, refs, lists, and conditional content.
5. [Stores](/concepts/stores): objects and arrays whose properties update independently, and how to update them through a draft.
6. [Avoid unnecessary effects](/guides/avoid-unnecessary-effects): the habit that separates working Solid code from fragile Solid code.
6. [Avoid unnecessary effects](/guides/avoid-unnecessary-effects): how to replace an effect that copies one value into another with a derivation.
7. [Async reactivity](/concepts/async-reactivity) and [Boundaries](/concepts/boundaries): loading data, showing fallbacks, and keeping the current screen visible while new data arrives.
8. [Mutations](/concepts/mutations): writing to a server with actions and optimistic stores, without changing the components that render the data.
9. [State management](/guides/state-management): where each piece of state lives, from a modal flag to the signed-in user.
Expand Down
5 changes: 2 additions & 3 deletions src/routes/(1)getting-started/(0)quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default function App() {
}
```

Three things in this file come up again and again in Solid:
These parts of the file come up again throughout Solid:

- `props.children` is where the matched page renders.
Layouts in Solid are ordinary components that render their children.
Expand Down Expand Up @@ -200,7 +200,6 @@ The button no longer updates, and the browser console shows a warning:
Move it into a tracking scope (JSX, a memo, or an effect's compute function).
```

This is the one rule that makes the rest of Solid make sense.
A component body runs once, so a value read there is a snapshot.
A read inside JSX, a memo, or an effect's compute function is tracked, and tracked reads update.
The [Reactivity](/concepts/reactivity) page explains why, and what to do when you need a value in the component body anyway.
Expand Down Expand Up @@ -282,7 +281,7 @@ describe("<Counter />", () => {

Run it with `npm test`.
If you kept the step input, `getByRole("button")` still finds the only button.
The `flush()` call matters: Solid applies DOM updates in a batch after the event handler returns, so a test that asserts immediately after `fireEvent.click` sees the old text.
The test needs the `flush()` call because Solid applies DOM updates in a batch after the event handler returns, so a test that asserts immediately after `fireEvent.click` sees the old text.
The [Testing guide](/guides/testing) covers this and the other environments.

Run `npm run build`.
Expand Down
15 changes: 6 additions & 9 deletions src/routes/(2)concepts/(0)reactivity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ In the [Quick start](/getting-started/quick-start) you saw two versions of the s
One read `count()` inside the JSX and updated on every click.
The other read `count()` in the component body, and the button froze at zero while the console warned that the read "will not update".

This page explains why.
Solid does not re-run components.
It runs each component once, records which reactive values each expression reads, and re-runs only those expressions when the values they read change.
The reason is that Solid runs each component once, records which reactive values each expression reads, and re-runs only those expressions when the values they read change.
The place where you read a value decides whether Solid can see the read.

The examples use a small shopping cart: a line item with a price and a quantity, a subtotal, and a shipping estimate.
Expand Down Expand Up @@ -53,7 +51,7 @@ A read inside a tracking scope subscribes that scope to the signal.
A read anywhere else returns the current value and subscribes nothing.

The component body is not a tracking scope.
That is deliberate: the body runs once to set up the component, and Solid must not re-run it, because that would recreate every signal and child inside it.
The body runs once to set up the component, and Solid must not re-run it, because that would recreate every signal and child inside it.
So this version reads the quantity once and never again:

```tsx
Expand All @@ -70,8 +68,7 @@ In development Solid warns:
Move it into a tracking scope (JSX, a memo, or an effect's compute function).
```

The fix is to move the read, not the value.
Wrap the calculation in a function and call the function from the JSX:
To fix it, wrap the calculation in a function and call the function from the JSX:

```tsx
const subtotal = () => quantity() * price;
Expand Down Expand Up @@ -122,7 +119,7 @@ function createSignal<T>(value: T) {
```

`read` can only record a subscriber if a tracking scope is running at the moment it is called.
A read in the component body runs when `currentScope` is empty, so it returns the value and records nothing; that is the whole reason the place you read matters.
A read in the component body runs when `currentScope` is empty, so it returns the value and records nothing.
:::

## Derived values
Expand Down Expand Up @@ -150,7 +147,7 @@ const total = createMemo(() => subtotal() * (1 - discount()));
When `quantity` changes, `subtotal` recomputes.
`total` recomputes because it read `subtotal()`.
When the user types `SAVE1` on the way to `SAVE10`, `discount` recomputes and produces `0` again; because the result is equal to the last one, `total` is not notified.
That equality check is what makes a memo a useful boundary in a chain of derivations.
That equality check lets a memo stop an update partway through a chain of derivations.

A memo's function must not write to signals or stores.
It runs during an update, and a write inside it throws `[REACTIVE_WRITE_IN_OWNED_SCOPE]` in development.
Expand Down Expand Up @@ -245,7 +242,7 @@ The guide [Avoid unnecessary effects](/guides/avoid-unnecessary-effects) works t
Every memo, effect, and cleanup belongs to an owner.
A component is an owner.
When Solid removes a component from the page, it disposes everything the component created: subscriptions are dropped, cleanups run, and the memos and effects stop.
This is why a component can create an effect and never think about tearing it down.
This is why a component can create an effect without tearing it down by hand.

The owner is whatever is running when the primitive is created.

Expand Down
10 changes: 4 additions & 6 deletions src/routes/(2)concepts/(1)components-and-jsx.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ description: "Components as functions that run once: props, children, control fl

A Solid component is a function that runs once.
It sets up state, returns JSX, and is not called again for the life of the component.
Every rule below about props, children and control flow follows from that: the function body will not run a second time.
Every rule below about props, children and control flow follows from that.
[Reactivity](/concepts/reactivity) explains how the JSX keeps updating after the function has returned.
This page covers props, events, refs, lists and conditions, and composition, continuing the shopping cart from that page.
The examples continue the shopping cart from that page.

## How JSX executes

Expand Down Expand Up @@ -71,15 +71,13 @@ function Cart() {
}
```

`quantity={quantity()}` looks like it reads the signal once, when `Cart` runs.
It does not.
The compiler turns a dynamic attribute into a getter on the props object, so the read of `quantity()` happens when `LineItem` reads `props.quantity`, inside its JSX, in a tracking scope.
`quantity={quantity()}` looks like it reads the signal once, when `Cart` runs, but the compiler turns a dynamic attribute into a getter on the props object, so the read of `quantity()` happens when `LineItem` reads `props.quantity`, inside its JSX, in a tracking scope.
That is why `props.quantity` in the child updates when the parent's signal changes, even though neither function runs again.

This works only as long as the read stays on the props object.

:::pitfall[Destructuring props reads them once]
The shortcut that works on a plain object is the one that breaks here:
Destructuring works on a plain object but not on props:

```tsx
// Avoid: both read props.quantity in the component body, once
Expand Down
3 changes: 1 addition & 2 deletions src/routes/(2)concepts/(2)stores.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ setCart((draft) => {
Three properties changed, and three subscriptions are notified: the coupon reader, the Mug quantity input, and the `length` reader.
The T-shirt row is not touched.

The habit from immutable state is to rebuild the collection.
That habit undoes what the store gives you:
The habit from immutable state is to rebuild the collection, which replaces the changed item with a new object:

```ts
// Avoid: a new object for the changed item, so For rebuilds that row
Expand Down
35 changes: 13 additions & 22 deletions src/routes/(2)concepts/(3)async-reactivity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Once a memo is async, a write to any input it depends on is held until the memo
The page never shows a mix of old and new data.
If you come from Solid 1 or from a pattern where every panel fetched and showed its own spinner on its own schedule, this is the part that will surprise you; the [in-flight work](#settled-view-and-in-flight-work) section shows it and the choices it gives you.

This page explains what async computations buy you and what you still have to decide.
The examples continue the shopping cart from the [Reactivity](/concepts/reactivity) page.

## A memo that returns a promise
Expand Down Expand Up @@ -42,8 +41,7 @@ function ProductDetail(props: { id: string }) {
}
```

`product()` has type `Product`, not `Product | undefined` and not `Promise<Product>`.
There is no loading flag to check and no optional chaining.
`product()` has type `Product`, not `Product | undefined` or `Promise<Product>`, so the JSX needs no loading flag and no optional chaining.
If the expression is running, the value is there.

Until the first result arrives, a read of `product()` reports that the value is not ready, and the nearest [`Loading`](/reference/solid-js/components-jsx/loading) boundary renders its fallback in place of the content.
Expand Down Expand Up @@ -79,8 +77,7 @@ function ProductLayout(props: { product: Product }) {
}
```

`product={product()}` looks like it calls the accessor before the data exists.
It does not; the expression is evaluated when `ProductDetail` reads `props.product.name`.
`product={product()}` looks like it calls the accessor before the data exists, but the expression is evaluated when `ProductDetail` reads `props.product.name`.
So `ProductLayout` renders immediately, `CategoryNav` renders immediately, and only the detail pane waits.

Derived values behave the same way:
Expand All @@ -103,7 +100,7 @@ No `await` and no promise type appear anywhere between the fetch and the JSX.

## Fetch high, block low

Because passing a value costs nothing, the two decisions that usually fight each other come apart:
Because passing a value costs nothing, where to fetch and where to block become separate decisions:

- Where to create the async value is a performance decision.
Higher in the tree means the request starts earlier and can run in parallel with other work.
Expand Down Expand Up @@ -178,7 +175,7 @@ const brand = createMemo(() => fetchBrand(product().brandId));
```

`brand` cannot start until `product` resolves, because the id comes from the response.
That is sequential because the data is sequential, and the dependency is visible in the code.
That dependency is visible in the code.
Development builds can flag long chains like this with the `ASYNC_WATERFALL` diagnostic when attribution is enabled; see [Debugging reactivity](/guides/debugging-reactivity).

## Settled view and in-flight work
Expand All @@ -198,9 +195,6 @@ The boundary controls what renders when no settled answer exists; it does not cr

### What the hold means for a shared input

The hold is simple to reason about for one memo and one view.
It is worth seeing for several.

A dashboard has a period selector and three panels.
Each panel fetches its own data from the selected period, and the fetches take 200 milliseconds, one second, and ten seconds:

Expand Down Expand Up @@ -231,13 +225,12 @@ For those ten seconds the page shows the new period's summary beside the old per

In Solid the write to `period` is held.
The selector keeps showing the old period, every panel keeps its old content, and at ten seconds the selector and all three panels change together.
Nothing on the page ever disagrees about which period it is showing.
The cost is that the fast panel waits for the slow one, and if nothing on the page reacts to the click, the page looks dead for ten seconds.

Neither behavior is right for every screen.
A detail page, a form, or a set of totals that must add up should change together.
A set of independent widgets should not have to.
The question to ask is about the update, not about any one request, and it is one your designer can answer:
The question covers the whole update, and a designer can answer it:

**While this update is in flight, what should the user see?**

Expand All @@ -248,7 +241,7 @@ The question to ask is about the update, not about any one request, and it is on
| A placeholder in place of the content for the changed subject | [`<Loading on={key}>`](#show-a-placeholder-again-loading-on) around that content. |

The first two rows are the same screen: the tab highlight moves at once, the content stays and dims, the content swaps when it is ready.
That is the standard interaction for a held update, and the rest of this section shows the pieces.
That is the standard interaction for a held update.
The third row is for content that should not stay on screen when its subject changes, such as a panel for a different account.

The hold applies only to updates.
Expand Down Expand Up @@ -311,8 +304,7 @@ function Search() {

Typing changes `query`, `results` starts a new request, and the previous results stay on screen with the `stale` class until the new ones land.

`isPending` is a question, not a piece of state.
It can be asked about the async source, about a memo derived from it, about a prop several components down, or about the signal whose write started the update:
`isPending` can be asked about the async source, about a memo derived from it, about a prop several components down, or about the signal whose write started the update:

```tsx
function App() {
Expand All @@ -332,7 +324,7 @@ function App() {
Here the fetch lives inside `ProductPage`.
`App` knows nothing about it, but `isPending(selectedId)` is `true` from the moment the write happens until everything downstream has settled, because the write is what is being held.

Notice that the list highlight does not move on click either.
The list highlight does not move on click either.
`selectedId()` holds its old value like everything else, so the highlighted row always matches the content on screen.
When the design wants the highlight to move immediately, read the input with [`latest`](#show-the-input-now-latest), covered next.

Expand Down Expand Up @@ -383,7 +375,6 @@ Before any value exists, the read still follows the normal `Loading` path.

Read the two sides together: `latest` on the control, the plain read on the content, `isPending` between them to dim the content.
The highlight moves at once, the old product stays visible and dims, and the new product replaces it when its data is ready.
This is the everyday shape of a held update, not a special case.

`latest` belongs on inputs.
Reading data through `latest` shows a result before the rest of the update has agreed on it, so a panel can show the new period's numbers while the selector still shows the old period.
Expand All @@ -403,9 +394,9 @@ The `on` prop is a dependency list: the boundary tracks the expression, and a ch
</Loading>
```

A `Loading` boundary is the update's chance to finish without waiting for that content: the region shows its fallback and waits on its own.
Normally the update takes that chance only on the region's first load.
`on` extends the offer to later updates: when `accountId()` changes and the new account is not ready, the boundary gives up its old content and takes the wait back from the update.
A `Loading` boundary lets an update finish without waiting for the content inside it: the region shows its fallback and waits on its own.
Without `on`, that happens only on the region's first load.
With `on`, it also happens on later updates: when `accountId()` changes and the new account is not ready, the boundary drops its old content, and the update stops waiting on that region.
If the panel was the only thing waiting, the update finishes at once.
Controls outside the boundary see the new id, the panel shows its skeleton, and the content arrives when the account does.
Pending work caused by anything `on` does not read, such as a refresh of the same account, leaves the content in place as usual.
Expand All @@ -432,12 +423,12 @@ When the very same data is also read outside the boundary, the frame waits on th
```

In the `Avoid` version, picking another account looks like nothing happened until the account loads, then the heading and the details change together; the `on` had no visible effect because the heading kept the frame waiting on the data the boundary was about to show a skeleton for.
Development reports this as `LOADING_ON_OUTSIDE_HOLD`, naming the source, and the fix is structural: move the outside read under the boundary so one hold owns the data.
Development reports this as `LOADING_ON_OUTSIDE_HOLD`, naming the source, and the fix is to move the outside read under the boundary so one hold owns the data.
The `Prefer` version is that fix.

So the rule for `on` is that the boundary encloses every reader of the data it waits on.
Other content waiting on the same input through its own data is fine; the skeleton arrives with the frame that content lands in.
That leaves a design choice rather than a rule about where boundaries go:
That leaves a design choice about where boundaries go:

- One boundary with `on` around every reader of the subject.
The whole subject shows one skeleton and appears together.
Expand Down
5 changes: 2 additions & 3 deletions src/routes/(2)concepts/(4)boundaries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ With one boundary around a whole product page, a slow recommendations query keep
With no boundary at all, an unhandled error halts the reactive system and nothing on the page updates again.
Boundaries decide how much of the page a pending or failed read affects.
[`Loading`](/reference/solid-js/components-jsx/loading) renders a fallback while async reads in its subtree have no value, [`Errored`](/reference/solid-js/components-jsx/errored) renders a fallback when its subtree throws, and [`Reveal`](/reference/solid-js/components-jsx/reveal) decides the order in which sibling regions appear.
Where you put each one is the design decision this page is about.

```tsx
import { Errored, Loading, createMemo, createSignal } from "solid-js";
Expand Down Expand Up @@ -50,7 +49,7 @@ On first render the panel shows "Loading product…" and the picker is usable.
When the request resolves the product appears; when it rejects, the error message and a **Retry** button appear in the same place.
Pick another product and the panel shows the loading text again while the picker keeps working.

Three rules explain that behavior.
That behavior follows from these rules.
A boundary handles status produced by reads in its own subtree, and the nearest matching boundary handles it.
Loading and error status are separate, so `Loading` does not hide an error and `Errored` does not replace loading UI; both can protect the same region.
Controls outside the boundary are outside its fallback, so the picker stays on screen in every state.
Expand Down Expand Up @@ -92,7 +91,7 @@ Pending work caused by anything `on` does not read, such as a refresh of the sam
The fallback appears with the same frame as the change that caused it.
When nothing else is waiting on that change, that is at once.
When other content outside the boundary is waiting on the same change, the page swaps when that content is ready, and the boundary shows its skeleton from that swap if its own content is still loading.
When the very same data is also read outside the boundary, the frame waits on it and the fallback can never appear; development warns `LOADING_ON_OUTSIDE_HOLD`, and the fix is structural: move the outside read under the boundary so one hold owns the data.
When the very same data is also read outside the boundary, the frame waits on it and the fallback can never appear; development warns `LOADING_ON_OUTSIDE_HOLD`, and the fix is to move the outside read under the boundary so one hold owns the data.
The [Async reactivity](/concepts/async-reactivity#show-a-placeholder-again-loading-on) page shows each case.

:::pitfall[Passing the accessor to on instead of reading it]
Expand Down
Loading
Loading