Skip to content
Open
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
44 changes: 42 additions & 2 deletions scripts/extract-solid-ref.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ const ADVANCED_ROUTES = {
"Advanced / Interop & Async",
],
flatten: ["advanced/interop-async/flatten.mdx", "Advanced / Interop & Async"],
from: ["advanced/interop-async/from.mdx", "Advanced / Interop & Async"],
NotReadyError: [
"advanced/interop-async/not-ready-error.mdx",
"Advanced / Interop & Async",
Expand Down Expand Up @@ -563,6 +564,7 @@ const FOLD_INTO = {
EffectFunction: "createEffect",
EffectOptions: "createEffect",
ExternalSource: "enableExternalSource",
FromSource: "from",
ExternalSourceConfig: "enableExternalSource",
ExternalSourceFactory: "enableExternalSource",
HydrationProjectionOptions: "createProjection",
Expand Down Expand Up @@ -632,6 +634,10 @@ const FOLD_INTO = {
};

const HIDDEN_EXPORTS = new Set([
// `@experimental`: the shared render behind it is the frames server half,
// which the reference excludes with the rest of frames. On the client it
// is exactly `live(fn)`.
"broadcast",
// Compiler slot protocol; not part of the application-facing reference.
"SLOT_VALUE",
"SLOT_MARKER",
Expand Down Expand Up @@ -725,6 +731,9 @@ const HIDDEN_EXPORTS = new Set([
"setAttribute",
"setAttributeNS",
"setProperty",
// Dev-compiled output's stateful property write (`@internal` in client.ts;
// the server entry re-exports a stub under the same name without JSDoc).
"setStatefulProperty",
"setStyleProperty",
"style",
"unregisterDelegatedContainer",
Expand Down Expand Up @@ -1078,6 +1087,9 @@ const PREFERRED_SOURCE_PATHS = {
ServerErrorContext: "packages/web/src/server.ts",
ServerErrorHook: "packages/web/src/server.ts",
ServerErrorsConfig: "packages/web/src/server.ts",
// The engine's JSDoc carries the contract; solid-js only adds the
// hydration brand, which the caveats state.
from: "packages/signals/src/from.ts",
};

const ENTRY_EXAMPLES = {
Expand Down Expand Up @@ -1443,6 +1455,10 @@ async function exportReport() {
// Shape: { symbol: { paramName: "text" | { type?, text? } } }. An object
// can pin the displayed type when the overload union is noisy.
const PARAM_DOCS = {
from: {
source:
"A subscribe function `(next, error, complete) => unsubscribe`, an object with `subscribe(observer)` such as an Observable, or an `AsyncIterable`. See `FromSource` below.",
},
createSignal: {
value:
"Initial value. Pass a function through the `fn` form instead; a function here is rejected by the types.",
Expand Down Expand Up @@ -1790,11 +1806,15 @@ const ENTRY_CAVEATS = {
isPending: [
"Reports on updates to a value that exists. Before a first value, the read inside `fn` follows the surrounding `Loading` path.",
"A bare `refresh()` does not flip `isPending`; pair it with `affects()` to make the re-ask visible.",
"An errored expression is not pending: `isPending` returns `false` and does not rethrow the error. Read the value itself inside an `Errored` boundary to show the failure.",
],
latest: [
"Use it on controls, not on content. Data read through `latest` can show a new answer before the rest of the update agrees with it.",
"Before any value exists, the read still suspends like a normal read.",
],
from: [
"During hydration, a subscription read on the server reconnects on the client once its section has hydrated, instead of staying at the server's value.",
],
untrack: [
"Untracking a read does not stop the computation from re-running for its other tracked reads.",
"Untracking does not opt out of async settlement. An initial `NotReadyError` still suspends the owning computation and retries it when the source first settles.",
Expand Down Expand Up @@ -1833,6 +1853,7 @@ const ENTRY_CAVEATS = {
"A plain `await` leaves the transaction. Put a bare `yield` before any writes that follow an `await`, or use `yield` in place of `await`.",
"Do not call `flush()` inside the generator; it drains the transaction mid-step.",
"Navigation-shaped updates (write an input, read the async result) do not need an action. A plain setter call is held automatically.",
"A failure rejects the returned promise and does not reach an `Errored` boundary. Catch it inside the generator, or with `.catch()` where the action is called.",
],
affects: [
"Marking does not change the value or trigger a refetch; it only reports pending. Pair with `refresh()` to re-ask.",
Expand Down Expand Up @@ -1891,6 +1912,7 @@ const ENTRY_CAVEATS = {
Errored: [
"The boundary catches errors thrown while rendering its subtree and errors that travel through the reactive graph, including rejected async reads. Handle errors in event handlers where they happen.",
"`reset()` recomputes the failing sources. If the cause persists, the boundary errors again.",
"While a `reset()` retry is pending, the fallback stays on screen, so a `Loading` inside the boundary does not show. To show a placeholder during the retry, put `Loading` outside `Errored` with an `on` value that the retry changes.",
"Errors thrown from the fallback itself go to the parent `Errored`.",
],
Loading: [
Expand Down Expand Up @@ -1995,6 +2017,16 @@ const REACTIVITY_PROBLEMS = {
"/guides/debugging-reactivity#every-update-stopped-after-an-error",
],
};
const BOUNDARY_PROBLEMS = {
retryKeepsError: [
"Retry keeps showing the error instead of a loading state",
"/concepts/boundaries#retry-keeps-showing-the-error-instead-of-a-loading-state",
],
actionNotCaught: [
"A failed action does not show the `Errored` fallback",
"/concepts/mutations#a-failed-action-does-not-show-the-errored-fallback",
],
};
const COMPONENT_PROBLEMS = {
childNotUpdating: [
"A child does not update when the parent's signal changes",
Expand Down Expand Up @@ -2083,7 +2115,11 @@ const ENTRY_PROBLEMS = {
reconcile: [LIST_PROBLEMS.loseFocus, LIST_PROBLEMS.wholeList],
merge: [COMPONENT_PROBLEMS.childNotUpdating],
omit: [COMPONENT_PROBLEMS.childNotUpdating],
action: [REACTIVITY_PROBLEMS.deadClick, FORM_PROBLEMS.errorsVanish],
action: [
REACTIVITY_PROBLEMS.deadClick,
FORM_PROBLEMS.errorsVanish,
BOUNDARY_PROBLEMS.actionNotCaught,
],
affects: [REACTIVITY_PROBLEMS.deadClick],
refresh: [REACTIVITY_PROBLEMS.deadClick],
children: [COMPONENT_PROBLEMS.childNotUpdating],
Expand All @@ -2095,7 +2131,11 @@ const ENTRY_PROBLEMS = {
LIST_PROBLEMS.wholeList,
],
Repeat: [LIST_PROBLEMS.wrongIndex],
Errored: [REACTIVITY_PROBLEMS.stoppedAfterError],
Errored: [
REACTIVITY_PROBLEMS.stoppedAfterError,
BOUNDARY_PROBLEMS.retryKeepsError,
BOUNDARY_PROBLEMS.actionNotCaught,
],
Loading: [REACTIVITY_PROBLEMS.deadClick],
Portal: [COMPONENT_PROBLEMS.refCallback],
respond: [FORM_PROBLEMS.respondInternalError, FORM_PROBLEMS.errorsVanish],
Expand Down
24 changes: 23 additions & 1 deletion src/routes/(2)concepts/(3)async-reactivity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,27 @@ With attribution enabled, development builds report that case as `SILENT_HOLD` a

If the expression has no settled answer yet, the read inside `isPending` still follows the surrounding `Loading` path; `isPending` reports on updates to a value that exists, not on the first load.

An error is not pending either.
When the request rejects, `isPending(results)` returns `false` and does not rethrow the error, so a region that reads `results` only through `isPending` never reports the failure:

```tsx
// Avoid: the only read of results is the probe
<Errored fallback={<p>Search failed.</p>}>
<p class={{ stale: isPending(results) }}>Showing results for {query()}</p>
</Errored>

// Prefer: the region also reads the value, so a rejection reaches Errored
<Errored fallback={<p>Search failed.</p>}>
<ul class={{ stale: isPending(results) }}>
<For each={results()}>{(product) => <li>{product.name}</li>}</For>
</ul>
</Errored>
```

In the `Avoid` version a failed search drops the `stale` class, and the paragraph stays on screen as if the search had succeeded.
In the `Prefer` version the `results()` read inside `<For>` throws the error, and the boundary shows "Search failed." in place of the list.
Keep a plain read of the value next to each `isPending` probe, inside the [`Errored` boundary](#work-rejects-errored) that should report it.

## Show the input now: `latest`

A held update keeps the input's old value visible along with the old content.
Expand Down Expand Up @@ -464,7 +485,8 @@ A loading boundary does not consume errors, and an error boundary does not repla
An error is a status of that part of the graph, not a terminal state.
When the data underneath changes, because an input signal changed or a `refresh` landed, the boundary retries and the content returns.
`reset` retries the failed sources rather than showing the same error again.
[Boundaries](/concepts/boundaries) covers placement and recovery in detail.
While the retry is in flight, the error fallback stays on screen; the `Loading` inside the boundary does not replace it.
[Boundaries](/concepts/boundaries#recovery) covers placement and recovery in detail, including how to show a placeholder during a retry.

## Read every input before the first `await`

Expand Down
40 changes: 40 additions & 0 deletions src/routes/(2)concepts/(4)boundaries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,41 @@ The error is the current status of that part of the graph, the same way "not rea
`reset` covers the case where nothing upstream will change on its own, such as a network outage.
It re-runs the failed sources, so a retry that hits the same error shows the same fallback, and a retry that succeeds shows the content.

While the retry is in flight, the error fallback stays on screen.
In the product panel above, clicking **Retry** leaves the error message and the button in place until the request settles; the `Loading` inside `Errored` does not show "Loading product…", because the error remains the region's answer until a new value replaces it.
When a retry should show the placeholder, move `Loading` outside `Errored` and give it an `on` value that the retry changes:

```tsx
const [productId, setProductId] = createSignal("mug");
const [attempt, setAttempt] = createSignal(0);
const product = createMemo(() => fetchProduct(productId()));

<Loading on={[productId(), attempt()]} fallback={<p>Loading product…</p>}>
<Errored
fallback={(error, reset) => (
<section>
<p>{String(error())}</p>
<button
onClick={() => {
setAttempt((n) => n + 1);
reset();
}}
>
Retry
</button>
</section>
)}
>
<h2>{product().name}</h2>
<p>{product().description}</p>
</Errored>
</Loading>;
```

Click **Retry** and "Loading product…" replaces the error message until the request settles; then the product or a new error message appears.
Picking another product shows the loading text as before.
The `attempt` write re-arms the boundary (see [Show a placeholder again](/concepts/async-reactivity#show-a-placeholder-again-loading-on)); without `on`, the outer `Loading` keeps the error message on screen too, because the region already has content.

:::caution[An error in the fallback is outside the boundary]
An error thrown while rendering an `Errored` fallback is not caught by that boundary.
A parent `Errored` can catch it.
Expand Down Expand Up @@ -267,6 +302,11 @@ Give the sibling its own `on`, or drop `on` and show the wait with `isPending`.
If the cause has not changed, the same error comes back.
When the fix is a different input, change the input; the boundary recovers on its own.

### Retry keeps showing the error instead of a loading state

An `Errored` boundary keeps its fallback while a `reset` retry is pending, so a `Loading` inside it cannot show its fallback for the retry.
Put `Loading` outside `Errored` and add a value that the **Retry** button changes to its `on`, as in [Recovery](#recovery).

### A region inside `Reveal` does not appear when its data is ready

The region is a slot in a `sequential` group and an earlier slot is not ready.
Expand Down
30 changes: 28 additions & 2 deletions src/routes/(2)concepts/(5)mutations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ The projection reads the durable items and folds in the `errors` map.
The map is not reactive by itself; `refresh(items)` re-runs the projection after each mutation and makes its current entries visible.

The action catches the expected failure because the row can recover locally.
An error the action does not catch, or an error thrown by the projection or the render, still reaches `Errored`.
An error thrown by the projection or the render still reaches `Errored`.
An error the action does not catch rejects the promise that `setGiftWrap` returns, and the `void` in the handler discards that promise, so no boundary sees the failure; see [A failed action does not show the Errored fallback](#a-failed-action-does-not-show-the-errored-fallback).

## Common problems

Expand All @@ -341,6 +342,31 @@ Make the tentative write inside the `action` that sends the request, and suspend
The store is a plain `createStore`, whose writes are durable even inside an action.
Use `createOptimisticStore` for state that should revert when the request settles.

### A failed action does not show the `Errored` fallback

An action call returns a promise, and an event handler does not wait for it.
When the action throws, the promise rejects outside the reactive graph: the optimistic overlay is dropped, the browser reports an unhandled rejection, and no `Errored` boundary shows its fallback.
`Errored` catches errors from reads and rendering, not from promises that a handler starts.

Handle the failure where the action is called.
Catch it inside the generator when the action can recover, as `setGiftWrap` does above, or attach `.catch()` to the call and write the error to a signal the UI reads:

```tsx
const [saveError, setSaveError] = createSignal<string>();

<button
onClick={() => {
setSaveError(undefined);
save().catch((error) => setSaveError(String(error)));
}}
>
Save
</button>;
<Show when={saveError()}>{(message) => <p role="alert">{message()}</p>}</Show>;
```

Click **Save** while the request fails and the message appears under the button; clicking again clears it before the next attempt.

### `flush()` inside an action throws `[FLUSH_IN_ACTION]`

An action's writes are held in its transaction until the action settles, so there is nothing for `flush()` to show.
Expand All @@ -362,6 +388,6 @@ Remove the call and assert after the action's promise resolves.

- [Forms](/guides/forms): the same actions behind a `<form>`, with progressive enhancement, validation, and errors returned from the server.
- [Mutations and responses](/building-apps/server-functions/mutations-and-responses): writing `api.add` as a `"use server"` function and returning redirects, statuses, and safe errors.
- [Boundaries](/concepts/boundaries): where `Errored` catches the errors an action does not, and how an errored region recovers.
- [Boundaries](/concepts/boundaries): where `Errored` catches errors from reads and rendering, and how an errored region recovers.
- [Data loading and mutations](/routing/solid-router/data): with Solid Router mounted, the same actions call server functions and the router applies their revalidation and redirects; `query` adds caching and `formAction` adds forms.
- [State management](/guides/state-management): where the cart, the user, and the filter live, and why `createCart()` runs inside a provider.
42 changes: 42 additions & 0 deletions src/routes/(5)guides/(12)typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,43 @@ The `!` says the variable is assigned before it is read, which holds when every
A component that exposes its element takes a `Ref<T>` prop, `T | ((val: T) => void) | undefined | Ref<T>[]` from `solid-js`, and passes it straight through to `ref`.
The [`ref` reference](/reference/solid-web/jsx-properties/ref) lists what the attribute accepts.

## Custom elements

The checkout form renders a `<date-picker>` custom element, and the checker reports `Property 'date-picker' does not exist on type 'JSX.IntrinsicElements'` until the tag is declared.
Declare it once by augmenting the `JSX` namespace of `@solidjs/web`, the package `jsxImportSource` points at:

```ts
// src/types/custom-elements.d.ts
export {};

declare module "@solidjs/web" {
namespace JSX {
interface IntrinsicElements {
"date-picker": HTMLAttributes<HTMLElement> & {
value?: string;
"prop:disabledDates"?: string[];
onDatechange?: (event: CustomEvent<{ value: string }>) => void;
};
}
}
}
```

With the file in place, `<date-picker value={date()} prop:disabledDates={holidays()} />` type-checks, `event.detail.value` in `onDatechange` is a `string`, and `prop:disabledDates={1}` reports `Type 'number' is not assignable to type 'string[]'`.
Inside the augmented namespace, `HTMLAttributes` is the namespace's own interface, which supplies the global attributes, `ref`, and the standard event handlers.

Two details decide whether the declaration applies:

- The file must be a module.
The `export {}` line, or any `import` or `export`, makes `declare module` an augmentation that merges into the package's types.
Without it the file is a global script, and the same block declares a new `@solidjs/web` module that replaces the package's types: imports such as `render` report `Module '"@solidjs/web"' has no exported member 'render'`, and the tag is still unknown.
- No `.ts` file next to it may share its name.
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.
[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

A `"use server"` function is typed like any async function, and the caller sees its declared return:
Expand Down Expand Up @@ -378,6 +415,11 @@ A function is not a valid child, so the checker reports it; add the parentheses.
The `ref` variable is typed `HTMLInputElement | undefined` and read in the component body, before the element exists.
Move the read into an event handler or `onSettled`, and declare the variable with `!` once every read happens after creation.

### `Module '"@solidjs/web"' has no exported member` after adding a declaration file

The `.d.ts` file that declares a custom element has no `import` or `export`, so its `declare module "@solidjs/web"` block replaces the package's types instead of adding to them.
Add `export {};` at the top of the file; see [Custom elements](#custom-elements).

## Recap

- Type `props` on the parameter, or use `Component<P>` when the component is a value; both produce the same checks.
Expand Down
7 changes: 6 additions & 1 deletion src/routes/(5)guides/(13)integrate-non-solid-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,12 @@ Declare it once, with the attribute, property, and event names the element suppo

```ts
// src/types/date-picker.d.ts
export {};

declare module "@solidjs/web" {
namespace JSX {
interface IntrinsicElements {
"date-picker": JSX.HTMLAttributes<HTMLElement> & {
"date-picker": HTMLAttributes<HTMLElement> & {
value?: string;
"prop:disabledDates"?: string[];
onDatechange?: (event: CustomEvent<{ value: string }>) => void;
Expand All @@ -144,6 +146,9 @@ declare module "@solidjs/web" {
}
```

The `export {}` line keeps the file a module, so the block adds to the types of `@solidjs/web` instead of replacing them.
[Custom elements](/guides/typescript#custom-elements) in the TypeScript guide covers the rules for this file.

## Render Solid into a foreign container

Two situations move Solid content into a DOM node Solid did not create.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const results = createMemo(() => searchProducts(query()));

- Reports on updates to a value that exists. Before a first value, the read inside `fn` follows the surrounding `Loading` path.
- A bare `refresh()` does not flip `isPending`; pair it with `affects()` to make the re-ask visible.
- An errored expression is not pending: `isPending` returns `false` and does not rethrow the error. Read the value itself inside an `Errored` boundary to show the failure.

## Common problems

Expand Down
Loading
Loading