solid-js 2.0.0-beta.1 → 2.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHEATSHEET.md +640 -0
- package/README.md +42 -188
- package/dist/dev.cjs +431 -283
- package/dist/dev.js +411 -286
- package/dist/server.cjs +701 -281
- package/dist/server.js +684 -284
- package/dist/solid.cjs +423 -255
- package/dist/solid.js +403 -258
- package/package.json +67 -39
- package/types/client/component.d.ts +64 -19
- package/types/client/core.d.ts +110 -34
- package/types/client/flow.d.ts +176 -42
- package/types/client/hydration.d.ts +514 -36
- package/types/index.d.ts +9 -12
- package/types/server/component.d.ts +11 -11
- package/types/server/core.d.ts +19 -14
- package/types/server/flow.d.ts +76 -21
- package/types/server/hydration.d.ts +34 -9
- package/types/server/index.d.ts +5 -7
- package/types/server/shared.d.ts +5 -1
- package/types/server/signals.d.ts +44 -19
- package/types/types.d.ts +15 -0
- package/types-cjs/client/component.d.cts +120 -0
- package/types-cjs/client/core.d.cts +141 -0
- package/types-cjs/client/flow.d.cts +234 -0
- package/types-cjs/client/hydration.d.cts +570 -0
- package/types-cjs/index.d.cts +17 -0
- package/types-cjs/package.json +3 -0
- package/types-cjs/server/component.d.cts +67 -0
- package/types-cjs/server/core.d.cts +49 -0
- package/types-cjs/server/flow.d.cts +115 -0
- package/types-cjs/server/hydration.d.cts +63 -0
- package/types-cjs/server/index.d.cts +10 -0
- package/types-cjs/server/shared.d.cts +50 -0
- package/types-cjs/server/signals.d.cts +87 -0
- package/types-cjs/types.d.cts +15 -0
- package/jsx-runtime.d.ts +0 -1
- package/types/jsx.d.ts +0 -4129
package/CHEATSHEET.md
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# Solid 2.0 — Cheatsheet
|
|
2
|
+
|
|
3
|
+
One-page reference for Solid 2.0. Every API exists in `solid-js` unless noted. DOM APIs are in `@solidjs/web`.
|
|
4
|
+
|
|
5
|
+
> **AI codegen warning.** Solid is **not React**, and 2.0 is **not 1.x**. Both priors are dominant bug sources here — distrust pattern-matching from either. The bottom of this file lists 2.0-specific corrections; read them before generating code.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Imports
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
createSignal,
|
|
14
|
+
createMemo,
|
|
15
|
+
createEffect,
|
|
16
|
+
createRoot,
|
|
17
|
+
For,
|
|
18
|
+
Show,
|
|
19
|
+
Switch,
|
|
20
|
+
Match,
|
|
21
|
+
Loading,
|
|
22
|
+
Errored,
|
|
23
|
+
Repeat,
|
|
24
|
+
Reveal,
|
|
25
|
+
createStore,
|
|
26
|
+
createProjection,
|
|
27
|
+
snapshot,
|
|
28
|
+
reconcile,
|
|
29
|
+
merge,
|
|
30
|
+
omit,
|
|
31
|
+
action,
|
|
32
|
+
createOptimistic,
|
|
33
|
+
createOptimisticStore,
|
|
34
|
+
isPending,
|
|
35
|
+
latest,
|
|
36
|
+
refresh,
|
|
37
|
+
untrack,
|
|
38
|
+
flush,
|
|
39
|
+
onSettled,
|
|
40
|
+
createContext,
|
|
41
|
+
useContext,
|
|
42
|
+
children,
|
|
43
|
+
lazy,
|
|
44
|
+
createUniqueId
|
|
45
|
+
} from "solid-js";
|
|
46
|
+
|
|
47
|
+
import { render, hydrate, Portal, dynamic, Dynamic } from "@solidjs/web";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Web projects should configure TypeScript with `"jsxImportSource": "@solidjs/web"`.
|
|
51
|
+
`solid-js` no longer provides `solid-js/jsx-runtime`; renderer packages own JSX types.
|
|
52
|
+
|
|
53
|
+
Old subpaths are gone:
|
|
54
|
+
`solid-js/web` → `@solidjs/web`. `solid-js/store` → `solid-js`. `solid-js/h` → `@solidjs/h`. `solid-js/html` → `@solidjs/html`. `solid-js/universal` → `@solidjs/universal`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Signals & memos
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Plain signal
|
|
62
|
+
const [count, setCount] = createSignal(0);
|
|
63
|
+
count(); // read (call it!)
|
|
64
|
+
setCount(1); // queues; read returns last committed until flush
|
|
65
|
+
setCount(c => c + 1); // updater form
|
|
66
|
+
|
|
67
|
+
// Readonly derived
|
|
68
|
+
const doubled = createMemo(() => count() * 2);
|
|
69
|
+
doubled();
|
|
70
|
+
|
|
71
|
+
// Writable derived ("writable memo")
|
|
72
|
+
const [value, setValue] = createSignal(() => props.initial);
|
|
73
|
+
|
|
74
|
+
// Options
|
|
75
|
+
createSignal(0, { ownedWrite: true }); // allow writes from inside owned scope
|
|
76
|
+
createSignal(0, { unobserved: () => cleanup() }); // fires when no subscribers
|
|
77
|
+
createMemo(fn, { lazy: true }); // defer first compute until read; autodispose when unobserved
|
|
78
|
+
createMemo(fn, { equals: (a, b) => a.id === b.id });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Reads update only after flush.** `setX(v); x()` returns the _previous_ value until the next microtask or `flush()`.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
setCount(1);
|
|
85
|
+
count(); // still 0
|
|
86
|
+
flush();
|
|
87
|
+
count(); // 1
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Effects
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// Two-arg form is the only form. Compute tracks; apply runs side effects.
|
|
96
|
+
createEffect(
|
|
97
|
+
() => count(), // compute (tracks)
|
|
98
|
+
(value, prev) => {
|
|
99
|
+
// apply (untracked)
|
|
100
|
+
el.title = value;
|
|
101
|
+
return () => {
|
|
102
|
+
/* cleanup */
|
|
103
|
+
}; // optional cleanup
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// With error handling
|
|
108
|
+
createEffect(() => fetchData(id()), {
|
|
109
|
+
effect: data => render(data),
|
|
110
|
+
error: (err, cleanup) => console.error(err)
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Run on next change only (skip initial)
|
|
114
|
+
createEffect(
|
|
115
|
+
() => count(),
|
|
116
|
+
v => log(v),
|
|
117
|
+
{ defer: true }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Schedule once after the current activity settles — the canonical
|
|
121
|
+
// "do this once and clean it up on dispose" primitive (replaces 1.x
|
|
122
|
+
// onMount + onCleanup for component-level lifecycle).
|
|
123
|
+
onSettled(() => {
|
|
124
|
+
const id = setInterval(tick, 1000);
|
|
125
|
+
return () => clearInterval(id);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`onSettled` works in component bodies (after first reactive settle) **and** in event handlers (defer work until the triggered transition settles). For component-level setup-and-teardown, **use `onSettled` with a returned cleanup, not `onCleanup` directly** — `onCleanup` is reserved for reactive cleanup inside computations (see Advanced).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Reactive control utilities
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
untrack(() => count()); // read without subscribing
|
|
137
|
+
flush(); // drain queued updates synchronously
|
|
138
|
+
isEqual(a, b); // default equality
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Stores
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const [store, setStore] = createStore({ user: { name: "A" }, list: [] });
|
|
147
|
+
|
|
148
|
+
// Draft-first setter (canonical)
|
|
149
|
+
setStore(s => {
|
|
150
|
+
s.user.name = "B";
|
|
151
|
+
s.list.push("x");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Return a value when mutation is awkward (filter/remove most often).
|
|
155
|
+
// Arrays replace by index + length; objects shallow-diff at the top level.
|
|
156
|
+
// No keyed reconciliation here — for that, use createProjection / createStore(fn).
|
|
157
|
+
setStore(s => s.list.filter(x => x !== "x"));
|
|
158
|
+
setStore(s => ({ ...s, list: [] }));
|
|
159
|
+
|
|
160
|
+
// Reconcile new data into a sub-tree (preserve identity)
|
|
161
|
+
setStore(s => {
|
|
162
|
+
reconcile(serverTodos, "id")(s.todos);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Derived stores (mirror signal/memo split)
|
|
166
|
+
const items = createProjection(async () => api.list(), [], { key: "id" }); // readonly
|
|
167
|
+
const [cache, setCache] = createStore(
|
|
168
|
+
draft => {
|
|
169
|
+
draft.x = compute();
|
|
170
|
+
},
|
|
171
|
+
{ x: 0 }
|
|
172
|
+
); // writable
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`undefined` is a real value in `merge` / setters — it overrides, not "skip".
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Props
|
|
180
|
+
|
|
181
|
+
**Props are reactive values, not accessors.** Two rules, one underlying model — and together the most common AI-generated bug class in Solid.
|
|
182
|
+
|
|
183
|
+
```jsx
|
|
184
|
+
const [count, setCount] = createSignal(0);
|
|
185
|
+
|
|
186
|
+
// 1. At the call site: pass the VALUE. Call accessors at the JSX boundary.
|
|
187
|
+
<Counter value={count()} /> // ✅
|
|
188
|
+
<Counter value={count} /> // ❌ child receives a function, not a number
|
|
189
|
+
|
|
190
|
+
// 2. In the child: read via `props.x`. The *property access* is what tracks.
|
|
191
|
+
function Counter(props) {
|
|
192
|
+
return <div>{props.value}</div>; // ✅ re-reads on each render
|
|
193
|
+
}
|
|
194
|
+
function Counter({ value }) { // ❌ destructure unwraps once, reactivity is dead
|
|
195
|
+
return <div>{value}</div>;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The rules are two sides of the same boundary: the parent collapses its accessors to values when handing off; the JSX runtime re-wraps `props` so that `props.value` re-reads on each access. Skip step 1 and the child gets a function; skip step 2 and the child reads once.
|
|
200
|
+
|
|
201
|
+
If you genuinely need to forward a getter (rare — render props, lazy slots), pass `getValue={() => count()}` and document it. Default to values.
|
|
202
|
+
|
|
203
|
+
### Helpers
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const merged = merge(defaults, props, overrides); // replaces mergeProps
|
|
207
|
+
const rest = omit(props, "class", "style"); // replaces splitProps
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Async
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// Async is just "any computation that returns a Promise / AsyncIterable"
|
|
216
|
+
const user = createMemo(() => fetchUser(id()));
|
|
217
|
+
// Reading user() suspends until ready; wrap in <Loading>.
|
|
218
|
+
|
|
219
|
+
// "Refreshing…" indicator (false during initial Loading)
|
|
220
|
+
isPending(() => user());
|
|
221
|
+
|
|
222
|
+
// Peek at the in-flight value during a transition
|
|
223
|
+
latest(id);
|
|
224
|
+
|
|
225
|
+
// Force recompute of a derived read after a server write
|
|
226
|
+
refresh(user);
|
|
227
|
+
refresh(() => query.user(id()));
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Actions & optimistic
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const [todos, setOptimisticTodos] = createOptimisticStore(() => api.list(), []);
|
|
236
|
+
|
|
237
|
+
const addTodo = action(function* (todo) {
|
|
238
|
+
setOptimisticTodos(s => {
|
|
239
|
+
s.push(todo);
|
|
240
|
+
}); // optimistic write
|
|
241
|
+
yield api.add(todo); // async work
|
|
242
|
+
refresh(todos); // re-derive
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Optimistic signal
|
|
246
|
+
const [name, setName] = createOptimistic("Alice");
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Optimistic writes revert when the transition completes.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Control-flow components
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// List, keyed by identity (default)
|
|
257
|
+
<For each={items()}>
|
|
258
|
+
{(item, i) => <Row item={item()} index={i()} />}
|
|
259
|
+
</For>
|
|
260
|
+
|
|
261
|
+
// List, non-keyed (replaces <Index>)
|
|
262
|
+
<For each={items()} keyed={false}>
|
|
263
|
+
{(item, i) => <Row item={item()} index={i()} />}
|
|
264
|
+
</For>
|
|
265
|
+
|
|
266
|
+
// List with custom key
|
|
267
|
+
<For each={items()} keyed={t => t.id} fallback={<Empty />}>
|
|
268
|
+
{item => <Row todo={item()} />}
|
|
269
|
+
</For>
|
|
270
|
+
|
|
271
|
+
// Range / count (no diffing) — i is a plain number, not an accessor
|
|
272
|
+
<Repeat count={store.items.length} fallback={<Empty />}>
|
|
273
|
+
{i => <Row name={store.items[i].name} />}
|
|
274
|
+
</Repeat>
|
|
275
|
+
|
|
276
|
+
// Conditional (function child receives narrowed accessor — call it!)
|
|
277
|
+
<Show when={user()} fallback={<Login />}>
|
|
278
|
+
{u => <Profile user={u()} />}
|
|
279
|
+
</Show>
|
|
280
|
+
|
|
281
|
+
// Branching
|
|
282
|
+
<Switch fallback={<NotFound />}>
|
|
283
|
+
<Match when={route() === "home"}><Home /></Match>
|
|
284
|
+
<Match when={route() === "profile"}>{() => <Profile />}</Match>
|
|
285
|
+
</Switch>
|
|
286
|
+
|
|
287
|
+
// Async boundary (replaces <Suspense>)
|
|
288
|
+
<Loading fallback={<Spinner />} on={id()}>
|
|
289
|
+
<Profile />
|
|
290
|
+
</Loading>
|
|
291
|
+
|
|
292
|
+
// Error boundary (replaces <ErrorBoundary>)
|
|
293
|
+
<Errored fallback={(err, reset) => <button onClick={reset}>retry</button>}>
|
|
294
|
+
<Page />
|
|
295
|
+
</Errored>
|
|
296
|
+
|
|
297
|
+
// Coordinate sibling Loadings (replaces <SuspenseList>).
|
|
298
|
+
// Default order is "sequential" — siblings reveal in registration order
|
|
299
|
+
// as each resolves. `collapsed` (sequential only) suppresses tail-sibling
|
|
300
|
+
// fallbacks past the frontier. Other orders: "together" | "natural".
|
|
301
|
+
<Reveal collapsed>
|
|
302
|
+
<Loading fallback={<S/>}><A/></Loading>
|
|
303
|
+
<Loading fallback={<S/>}><B/></Loading>
|
|
304
|
+
</Reveal>
|
|
305
|
+
|
|
306
|
+
// Dynamic component — factory form (canonical). Stable Component identity;
|
|
307
|
+
// composes with lazy(), routing, polymorphic patterns.
|
|
308
|
+
import { dynamic } from "@solidjs/web";
|
|
309
|
+
|
|
310
|
+
const Active = dynamic(() => isEditing() ? Editor : Viewer);
|
|
311
|
+
return <Active value={value()} />;
|
|
312
|
+
|
|
313
|
+
// JSX-wrapper convenience form. Use when the source is inline and you don't
|
|
314
|
+
// need to capture the component identity.
|
|
315
|
+
import { Dynamic } from "@solidjs/web";
|
|
316
|
+
<Dynamic component={isEditing() ? Editor : Viewer} value={value()} />
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`<For>` non-keyed: `item` and `i` are **accessors** — call them: `item()`, `i()`.
|
|
320
|
+
`<Repeat>`: `i` is a **plain number**.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Context, components, lazy
|
|
325
|
+
|
|
326
|
+
Context is for state scoped to a subtree of the component tree. **If you
|
|
327
|
+
want truly app-wide state, don't use Context — a module-scope signal/store
|
|
328
|
+
_is_ a global.** That's why the default-less form requires a Provider.
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
// Default-less — the canonical form. No Provider → ContextNotFoundError.
|
|
332
|
+
type TodosCtx = readonly [Store<Todo[]>, TodoActions];
|
|
333
|
+
const TodosContext = createContext<TodosCtx>();
|
|
334
|
+
|
|
335
|
+
function App() {
|
|
336
|
+
return (
|
|
337
|
+
<TodosContext value={createTodos()}>
|
|
338
|
+
{" "}
|
|
339
|
+
{/* the context IS the provider */}
|
|
340
|
+
<TodoList />
|
|
341
|
+
</TodosContext>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function TodoList() {
|
|
346
|
+
const [todos, { addTodo }] = useContext(TodosContext); // typed as TodosCtx
|
|
347
|
+
// ...
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
// Default form — only for primitive fallbacks (theme, locale, frozen config).
|
|
353
|
+
// Outside any Provider, useContext returns the default.
|
|
354
|
+
const Theme = createContext<"light" | "dark">("light");
|
|
355
|
+
|
|
356
|
+
function Page() {
|
|
357
|
+
const theme = useContext(Theme); // "light" | "dark"
|
|
358
|
+
return <div class={theme}>...</div>;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Don't write a `useTodos`-style wrapper that re-throws on missing Provider —
|
|
363
|
+
the default-less form already throws, and `useContext` is typed `T` (no
|
|
364
|
+
`| undefined`). The wrapper is React-flavored boilerplate that no longer
|
|
365
|
+
earns its keep.
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
// Resolve children once
|
|
369
|
+
const list = children(() => props.children);
|
|
370
|
+
list.toArray();
|
|
371
|
+
|
|
372
|
+
// Async component
|
|
373
|
+
const Heavy = lazy(() => import("./Heavy"));
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Component types:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
type Basic = Component<P>; // no implicit children
|
|
380
|
+
type Empty = VoidComponent<P>; // forbids children
|
|
381
|
+
type WithChildren = ParentComponent<P>; // optional renderer-neutral Element children
|
|
382
|
+
type Flow = FlowComponent<P, C>; // requires children of type C
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## DOM rendering
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import { render, hydrate, Portal } from "@solidjs/web";
|
|
391
|
+
|
|
392
|
+
const dispose = render(() => <App />, document.getElementById("root")!);
|
|
393
|
+
hydrate(() => <App />, document.getElementById("root")!);
|
|
394
|
+
|
|
395
|
+
<Portal mount={document.body}><Modal /></Portal>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Refs and directives
|
|
399
|
+
|
|
400
|
+
```jsx
|
|
401
|
+
// Element access
|
|
402
|
+
<button ref={el => (myButton = el)} />
|
|
403
|
+
|
|
404
|
+
// Directive factory (replaces use:)
|
|
405
|
+
<input ref={autofocus} />
|
|
406
|
+
<button ref={tooltip({ content: "Save" })} />
|
|
407
|
+
|
|
408
|
+
// Compose multiple
|
|
409
|
+
<button ref={[autofocus, tooltip({ content: "Save" })]} />
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Two-phase directive (recommended):
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
function titleDirective(source) {
|
|
416
|
+
// Setup phase (owned): create primitives.
|
|
417
|
+
let el;
|
|
418
|
+
createEffect(source, value => {
|
|
419
|
+
if (el) el.title = value;
|
|
420
|
+
});
|
|
421
|
+
// Apply phase (unowned): DOM writes only.
|
|
422
|
+
return nextEl => {
|
|
423
|
+
el = nextEl;
|
|
424
|
+
el.title = source();
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Attributes
|
|
430
|
+
|
|
431
|
+
```jsx
|
|
432
|
+
<video muted={true} /> // boolean = presence/absence
|
|
433
|
+
<video muted={false} />
|
|
434
|
+
<some-element enabled="true" /> // when platform requires the string
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Lowercase HTML attribute names. No `attr:` / `bool:` / `oncapture:` namespaces. Event handlers stay camelCase (`onClick`).
|
|
438
|
+
|
|
439
|
+
### Conditional classes — always use the array/object form
|
|
440
|
+
|
|
441
|
+
```jsx
|
|
442
|
+
<div class="card" /> // static string
|
|
443
|
+
<div class={{ active: isActive(), invalid: !valid() }} /> // object: toggle by truthiness
|
|
444
|
+
<div class={["card", props.class, { active: isActive() }]} /> // array: merge entries
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Array entries are always-on (or further nested arrays/objects). Object entries toggle by truthiness. There is no `classList` prop — the array+object form replaces it.
|
|
448
|
+
|
|
449
|
+
**Don't build class strings manually.** String concatenation, template literals, and `.join(" ")` over conditionals are the React/`classnames` reflex. Use the array+object form so conditions compose:
|
|
450
|
+
|
|
451
|
+
```jsx
|
|
452
|
+
<li class={["todo", { completed: props.todo.completed, errored: !!err() }]} />; // ✅
|
|
453
|
+
|
|
454
|
+
const cls = [
|
|
455
|
+
"todo", // ❌
|
|
456
|
+
props.todo.completed && "completed",
|
|
457
|
+
err() && "errored"
|
|
458
|
+
]
|
|
459
|
+
.filter(Boolean)
|
|
460
|
+
.join(" ");
|
|
461
|
+
return <li class={cls} />;
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## SSR (server entry)
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import { renderToString, renderToStringAsync, renderToStream, isServer, isDev } from "@solidjs/web";
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`Portal` throws on the server. `Reveal` `order="together"` and `collapsed` require streaming (`renderToStream` / `renderToStringAsync`).
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Diagnostics (dev mode)
|
|
477
|
+
|
|
478
|
+
Common dev-mode warnings/errors you may hit:
|
|
479
|
+
|
|
480
|
+
- **Top-level reactive read in component body** — read inside JSX or wrap in `untrack`/`createMemo`.
|
|
481
|
+
- **Write under owned scope** — move setters into event handlers / `onSettled` / `untrack`, or opt in with `{ ownedWrite: true }`.
|
|
482
|
+
- **Strict read untracked** — extract values in the compute phase; don't read store proxies inside the effect callback.
|
|
483
|
+
- **Multiple Solid instances** — single `solid-js` install required.
|
|
484
|
+
|
|
485
|
+
Each diagnostic has a code (see RFC 08 / runtime error message) — search the docs by code.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Advanced / escape hatches
|
|
490
|
+
|
|
491
|
+
Reach for these only when the named situation applies. **If you're not sure, you don't need them** — the common-path APIs above are the answer.
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
// Reactive cleanup inside a computation — runs before the next compute and
|
|
495
|
+
// on disposal. For component-level setup-and-teardown, use onSettled and
|
|
496
|
+
// return a cleanup; onCleanup is for library/primitive internals where the
|
|
497
|
+
// cleanup is tied to a reactive run, not a component lifecycle.
|
|
498
|
+
onCleanup(() => disposeReactiveResource());
|
|
499
|
+
|
|
500
|
+
// Deep tracking — only when an effect needs to react to *any* nested store change.
|
|
501
|
+
// Default store tracking is property-level (preferred).
|
|
502
|
+
createEffect(
|
|
503
|
+
() => deep(store),
|
|
504
|
+
snap => save(snap)
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// Plain (non-reactive) deep copy of a store. For serialization (JSON.stringify,
|
|
508
|
+
// localStorage, sending over the wire) and tests that need a plain-object
|
|
509
|
+
// assertion target. Inside reactive scopes, prefer reading individual values.
|
|
510
|
+
JSON.stringify(snapshot(store));
|
|
511
|
+
|
|
512
|
+
// Render-phase synchronous effect — for DOM bindings that must run during render
|
|
513
|
+
// (the runtime's own attribute/property bindings). For app code, use createEffect.
|
|
514
|
+
createRenderEffect(
|
|
515
|
+
() => props.title,
|
|
516
|
+
v => {
|
|
517
|
+
el.title = v;
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// Single-callback effect that may re-run in async situations.
|
|
522
|
+
// Rare; prefer createEffect.
|
|
523
|
+
createTrackedEffect(() => log(count()));
|
|
524
|
+
|
|
525
|
+
// One-shot tracked callback (advanced reactive patterns).
|
|
526
|
+
const track = createReaction(() => doWork());
|
|
527
|
+
track(() => count());
|
|
528
|
+
|
|
529
|
+
// Manually create an owned reactive scope. App code uses render(); reach for
|
|
530
|
+
// createRoot in tests, library setup, and other non-render entry points where
|
|
531
|
+
// you need an owner so reactive primitives can dispose properly.
|
|
532
|
+
createRoot(dispose => {
|
|
533
|
+
const [count, setCount] = createSignal(0);
|
|
534
|
+
// ...
|
|
535
|
+
dispose();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Detach a root from its parent (module singletons, external integrations only).
|
|
539
|
+
runWithOwner(null, () => {
|
|
540
|
+
/* ... */
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Get the current owner. Mostly used to capture and restore an owner across an
|
|
544
|
+
// async boundary inside library code.
|
|
545
|
+
const owner = getOwner();
|
|
546
|
+
runWithOwner(owner, () => {
|
|
547
|
+
/* ... */
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Wait for a reactive expression to settle (imperative code / tests).
|
|
551
|
+
const v = await resolve(() => user());
|
|
552
|
+
|
|
553
|
+
// Are we inside a refresh() cycle? Almost never needed in app code.
|
|
554
|
+
isRefreshing();
|
|
555
|
+
|
|
556
|
+
// Throw to signal "not ready" through the reactive graph (library authors).
|
|
557
|
+
throw new NotReadyError();
|
|
558
|
+
|
|
559
|
+
// 1.x-style path setter compat. Use only when migrating; draft-first is canonical.
|
|
560
|
+
setStore(storePath("user", "address", "city", "Paris"));
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## What changed from 1.x (the AI footgun list)
|
|
566
|
+
|
|
567
|
+
If your training data is 1.x, these are the corrections. **Read this before generating Solid 2.0 code.**
|
|
568
|
+
|
|
569
|
+
### Imports moved
|
|
570
|
+
|
|
571
|
+
- `solid-js/web` → `@solidjs/web`
|
|
572
|
+
- `solid-js/store` → `solid-js` (store APIs moved into core)
|
|
573
|
+
- `solid-js/h` / `solid-js/html` / `solid-js/universal` → `@solidjs/h` / `@solidjs/html` / `@solidjs/universal`
|
|
574
|
+
- `jsxImportSource: "solid-js"` → `"@solidjs/web"` for web JSX (`@solidjs/h` for hyperscript JSX)
|
|
575
|
+
|
|
576
|
+
### Renames
|
|
577
|
+
|
|
578
|
+
| 1.x | 2.0 |
|
|
579
|
+
| ------------------- | --------------------------------------------------------- |
|
|
580
|
+
| `Suspense` | `Loading` |
|
|
581
|
+
| `SuspenseList` | `Reveal` |
|
|
582
|
+
| `ErrorBoundary` | `Errored` |
|
|
583
|
+
| `mergeProps` | `merge` |
|
|
584
|
+
| `splitProps` | `omit` |
|
|
585
|
+
| `unwrap` | `snapshot` |
|
|
586
|
+
| `onMount` | `onSettled` |
|
|
587
|
+
| `createSelector` | `createProjection` (or `createStore(fn)`) |
|
|
588
|
+
| `equalFn` | `isEqual` |
|
|
589
|
+
| `getListener` | `getObserver` |
|
|
590
|
+
| `Context.Provider` | `<Context value={...}>` (context value _is_ the provider) |
|
|
591
|
+
| `classList={{...}}` | `class={{...}}` (object/array forms) |
|
|
592
|
+
|
|
593
|
+
### Removed (with replacements)
|
|
594
|
+
|
|
595
|
+
| Removed | Use instead |
|
|
596
|
+
| ---------------------------------- | ------------------------------------------------------------------- |
|
|
597
|
+
| `batch` | Default microtask batching; `flush()` to apply now |
|
|
598
|
+
| `createComputed` | `createMemo` / split `createEffect` / function-form `createSignal` |
|
|
599
|
+
| `createResource` | Async computations + `<Loading>` (`createMemo(() => fetchX(id()))`) |
|
|
600
|
+
| `startTransition`, `useTransition` | Built-in transitions; `isPending` / `<Loading>` / optimistic APIs |
|
|
601
|
+
| `on(...)` helper | Split effects (compute phase = explicit deps) |
|
|
602
|
+
| `onError` / `catchError` | `<Errored>` or effect `error` option |
|
|
603
|
+
| `produce` | Default — store setters are draft-first |
|
|
604
|
+
| `createMutable` / `modifyMutable` | `createStore` with draft setters |
|
|
605
|
+
| `from` / `observable` | Async iterables in computations / `createEffect` to push out |
|
|
606
|
+
| `Index` | `<For keyed={false}>` |
|
|
607
|
+
| `indexArray` | `mapArray` (handles non-keyed too) |
|
|
608
|
+
| `use:foo={x}` directives | `ref={foo(x)}` (or array `ref={[a, b(x)]}`) |
|
|
609
|
+
| `attr:` / `bool:` namespaces | Standard attribute behavior |
|
|
610
|
+
| `oncapture:` | `addEventListener(..., { capture: true })` |
|
|
611
|
+
| `resetErrorBoundaries` | Boundaries heal automatically |
|
|
612
|
+
|
|
613
|
+
### Behavior changes
|
|
614
|
+
|
|
615
|
+
- **`createEffect` takes two arguments now**: `(compute, apply)`. The single-arg form is gone — using it is an error.
|
|
616
|
+
- **Setters don't update reads immediately** — values become visible after the microtask flushes (or via `flush()`).
|
|
617
|
+
- **No writes inside owned scope** — writing a signal/store from inside a memo, effect compute, or component body throws in dev. Move writes to event handlers, `onSettled`, or untracked blocks. Opt in narrowly with `{ ownedWrite: true }` for internal state.
|
|
618
|
+
- **No top-level reactive reads in component body** — reading signals/props directly at the top of a component warns. Read inside JSX, a memo, or `untrack`.
|
|
619
|
+
- **Props are values, not accessors** — at the call site call accessors (`<X v={count()} />`, not `<X v={count} />`). The single most common AI-generated bug.
|
|
620
|
+
- **Don't destructure props** — `function Comp({ name })` warns; use `props.name` to keep reactivity. (Same root cause as above; see the Props section.)
|
|
621
|
+
- **`<For>` non-keyed children are accessors** — `(item, i) => ...` where `item` and `i` are functions. Call them: `item()`, `i()`.
|
|
622
|
+
- **`<Show>` / `<Match>` function children receive narrowed accessors** — also call them.
|
|
623
|
+
- **Stores: setters take a draft callback** — mutate the draft in place by default. Returning a new value is shallow (array index-replace, object top-level diff); reach for it for filter/remove. Keyed reconcile is a _projection-fn_ feature, not a setter feature.
|
|
624
|
+
- **`undefined` is a real value in `merge`** — it overrides rather than "skip this key".
|
|
625
|
+
- **Async lives in computations** — return a Promise/AsyncIterable from `createMemo`/`createStore(fn)`/`createProjection`. Reads suspend; wrap in `<Loading>`.
|
|
626
|
+
- **`Loading` is initial-only by default** — once content has rendered, revalidation keeps it visible. Use `isPending(() => x())` for "refreshing…" indicators. Use `<Loading on={key}>` to re-show fallback on key changes.
|
|
627
|
+
- **No `Suspense.Provider` or single error path** — async errors flow to `<Errored>` (or effect `error`); no inline `resource.error` branching.
|
|
628
|
+
- **`createRoot` is owned by parent by default** — disposed when parent disposes. To detach: `runWithOwner(null, fn)`.
|
|
629
|
+
- **Refs are functions** — `ref={el => ...}`. No `useRef`-style ref objects. Compose with arrays: `ref={[a, b]}`.
|
|
630
|
+
- **Boolean attributes are presence/absence** — `<video muted={false} />` removes the attribute.
|
|
631
|
+
- **Built-in attributes are lowercase** — `tabindex` not `tabIndex`. Event handlers stay camelCase (`onClick`).
|
|
632
|
+
- **In tests, `flush()` before asserting on signals** — `setCount(1); flush(); expect(count()).toBe(1)`.
|
|
633
|
+
- **Reactive primitives need an owner** — wrap test code in `createRoot(dispose => { ... })` or you'll leak.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## See also
|
|
638
|
+
|
|
639
|
+
- [`MIGRATION.md`](https://github.com/solidjs/solid/blob/main/documentation/solid-2.0/MIGRATION.md) — full beta-tester migration guide.
|
|
640
|
+
- [Solid 2.0 RFCs](https://github.com/solidjs/solid/tree/main/documentation/solid-2.0) — deep-dive design docs by subsystem.
|