Migrating to Solid 2.0
Edit this pageThis guide covers every breaking change in Solid 2.0 and shows how to update your code. Work through the quick checklist first, then use the detailed sections for each area.
Quick checklist
- Batching: Updates are batched by default via microtasks. Signal reads update immediately after a set, but DOM updates and effects are deferred until the microtask flush (or
flush()). - Effects:
createEffectuses a split model (compute → apply). Cleanup is returned from the apply function. - Lifecycle:
onMountis replaced byonSettled, which can return cleanup. - Async UI: Use
<Loading>for first readiness; useisPending()for "refreshing…" indicators. - Data fetching:
createResourceis removed. Use asynccreateMemoorcreateStore(fn). - Lists:
<Index>is removed — use<For keyed={false}>.<For>children receive accessors (item(),i()). - Stores: Prefer draft-first setters.
storePath()exists as a compatibility helper. - Plain values:
snapshot()replacesunwrap(). - DOM:
use:directives are removed. Userefdirective factories. - Helpers:
mergeProps→merge,splitProps→omit. - Components:
Suspense→Loading,ErrorBoundary→Errored. - Context:
Context.Provider→ use the context directly as the provider component.
Core behavior changes
Batching and reads
In Solid 2.0, updates are batched by default via microtasks. Signal reads update immediately after calling a setter, but DOM updates and effect runs are deferred until the next microtask or until flush() is called.
import { createSignal, createEffect, flush } from "solid-js"
const [count, setCount] = createSignal(0)
setCount(1)count() // 1 — reads are immediately up to date
// But effects and DOM updates are deferred:createEffect( () => count(), (value) => console.log("count =", value))// The effect hasn't run yet — call flush() to force it synchronouslyflush()The batch() function from 1.x is removed since batching is now the default. Use flush() when you need synchronous DOM updates or effect execution — most commonly in tests.
The batch() function from 1.x is removed since batching is now the default behavior.
Effects: split model
createEffect now takes two functions — a compute function that tracks reactive dependencies and returns a value, and an apply function that performs side effects with that value.
// 1.x — single functioncreateEffect(() => { document.title = name()})
// 2.0 — split: compute → applycreateEffect( () => name(), (value) => { document.title = value })Cleanup is returned from the apply function instead of using onCleanup:
// 1.xcreateEffect(() => { const id = setInterval(() => console.log(name()), 1000) onCleanup(() => clearInterval(id))})
// 2.0createEffect( () => name(), (value) => { const id = setInterval(() => console.log(value), 1000) return () => clearInterval(id) })Lifecycle: onMount → onSettled
onSettled replaces onMount. It runs after all async descendants have resolved and can return cleanup:
// 1.xonMount(() => { measureLayout()})
// 2.0onSettled(() => { measureLayout() const onResize = () => measureLayout() window.addEventListener("resize", onResize) return () => window.removeEventListener("resize", onResize)})onSettled cannot create nested reactive primitives (no createSignal, createEffect, etc. inside).
Dev warnings
Solid 2.0 introduces development-only warnings that catch subtle bugs early.
Top-level reactive reads
Reading reactive values at the top level of a component body (including destructuring props) will warn:
// ❌ warns in 2.0function Bad(props) { const n = props.count return <div>{n}</div>}
// ✅ read inside JSX or a reactive scopefunction Good(props) { return <div>{props.count}</div>}Destructuring props in the argument list also triggers the warning:
// ❌ warnsfunction Bad({ title }) { return <h1>{title}</h1>}
// ✅ keep the props objectfunction Good(props) { return <h1>{props.title}</h1>}Writes inside reactive scopes
Writing to signals or stores inside a reactive scope (effects, memos, component body) will warn:
// ❌ warns: writing from inside a memocreateMemo(() => setDoubled(count() * 2))
// ✅ derive instead of writing backconst doubled = createMemo(() => count() * 2)If you truly need to write a signal from an owned scope (not for app state), opt in with { pureWrite: true } on createSignal.
Async data and transitions
createResource → async computations
createResource is removed. Use async createMemo or the function form of createStore:
// 1.xconst [user] = createResource(id, fetchUser)
// 2.0const user = createMemo(() => fetchUser(id()))Wrap the consuming component in a <Loading> boundary:
<Loading fallback={<Spinner />}> <Profile user={user()} /></Loading>For list data that should reconcile by key, use the function form of createStore:
const [todos] = createStore(() => api.getTodos(), [], { key: "id" })Suspense → Loading, ErrorBoundary → Errored
// 1.x<Suspense fallback={<Spinner />}> <Profile /></Suspense>
<ErrorBoundary fallback={(err) => <p>{err.message}</p>}> <App /></ErrorBoundary>
// 2.0<Loading fallback={<Spinner />}> <Profile /></Loading>
<Errored fallback={(err) => <p>{err.message}</p>}> <App /></Errored>Initial loading vs. revalidation
<Loading>— handles initial "not ready yet" UI.isPending()— "stale while revalidating" indicator. Returns false during the initialLoadingfallback.
const refreshing = () => isPending(() => users())
<Show when={refreshing()}> <RefreshIndicator /></Show><Loading fallback={<Spinner />}> <UserList users={users()} /></Loading>Peeking in-flight values
Use latest() to read the most recent value even during a transition:
const latestId = () => latest(id)Mutations: action() + refresh()
In 1.x, mutations required ad-hoc patterns with startTransition and manual refetching. In 2.0, use action() with refresh():
// 1.xconst [data, { mutate, refetch }] = createResource(fetchData)startTransition(() => mutate(optimistic))await saveToServer()refetch()
// 2.0const save = action(function* (value) { setOptimistic(value) yield saveToServer() refresh(data)})For optimistic UI, combine with createOptimistic or createOptimisticStore:
const [todos] = createStore(() => api.getTodos(), { list: [] })const [optimisticTodos, setOptimisticTodos] = createOptimisticStore( () => snapshot(todos), { list: [] })
const addTodo = action(function* (todo) { setOptimisticTodos((s) => s.list.push(todo)) yield api.addTodo(todo) refresh(todos)})Stores
Draft-first setters
Store setters now use a produce-style draft by default:
// 1.x — path argumentssetStore("user", "address", "city", "Paris")
// 2.0 — draft-first (preferred)setStore((s) => { s.user.address.city = "Paris"})For gradual migration, storePath() provides the old path-argument ergonomics:
import { storePath } from "solid-js/store"
setStore(storePath("user", "address", "city", "Paris"))unwrap → snapshot
// 1.ximport { unwrap } from "solid-js/store"const plain = unwrap(store)
// 2.0import { snapshot } from "solid-js/store"const plain = snapshot(store)snapshot() returns a non-reactive plain object suitable for serialization. It generates distinct objects (not just unwrapped proxies).
Function forms
createSignal(fn) creates a writable derived signal:
const [count, setCount] = createSignal(0)const [doubled] = createSignal(() => count() * 2)createStore(fn) creates a derived store:
const [items] = createStore(() => api.listItems(), [])Control flow
Index → <For keyed={false}>
The <Index> component is removed. Use <For> with keyed={false}:
// 1.x<Index each={items()}> {(item, i) => <Row item={item()} index={i} />}</Index>
// 2.0<For each={items()} keyed={false}> {(item, i) => <Row item={item()} index={i()} />}</For>For children receive accessors
Both the item and index are now accessors — call them with ():
// 1.x<For each={items()}> {(item, i) => <span>{item.name} at {i}</span>}</For>
// 2.0<For each={items()}> {(item, i) => <span>{item().name} at {i()}</span>}</For>Function children in Show and Match
<Show when={user()} fallback={<Login />}> {(u) => <Profile user={u()} />}</Show>Props helpers
mergeProps → merge
// 1.ximport { mergeProps } from "solid-js"const merged = mergeProps(defaults, overrides)
// 2.0import { merge } from "solid-js"const merged = merge(defaults, overrides)In 2.0, undefined is treated as a real value. merge({ a: 1 }, { a: undefined }) results in a being undefined, not 1.
splitProps → omit
// 1.ximport { splitProps } from "solid-js"const [local, rest] = splitProps(props, ["class", "style"])
// 2.0import { omit } from "solid-js"const rest = omit(props, "class", "style")DOM changes
Attribute-first model
Solid 2.0 uses attributes by default (not properties). The attr: and bool: namespaces are removed:
// 1.x<video attr:muted={true} /><input bool:disabled={isDisabled()} />
// 2.0 — standard attributes<video muted={true} /><input disabled={isDisabled()} />classList → class
The classList attribute is removed. Use the class attribute with object or array forms:
// 1.x<div class="card" classList={{ active: isActive(), disabled: isDisabled() }} />
// 2.0<div class={["card", { active: isActive(), disabled: isDisabled() }]} />Directives: use: → ref factories
The use: directive syntax is removed. Use ref directive factories:
// 1.x<button use:tooltip={{ content: "Save" }} />
// 2.0<button ref={tooltip({ content: "Save" })} />Multiple directives compose via arrays:
<button ref={[autofocus, tooltip({ content: "Save" })]} />oncapture: removed
Capture-phase event listeners via oncapture: are no longer available.
Context
Context as provider
Context.Provider is removed. The context itself is the provider component:
// 1.xconst Theme = createContext("light")<Theme.Provider value="dark">{props.children}</Theme.Provider>
// 2.0const Theme = createContext("light")<Theme value="dark">{props.children}</Theme>Removed APIs
createResource
Replaced by async createMemo, function-form createStore, and <Loading> boundaries. See Fetching Data for full patterns.
batch
Removed — batching is now the default. Use flush() when you need synchronous updates.
on helper
Removed — the split effect model (separate compute and apply functions) replaces the need for explicit dependency declaration.
createSelector
Replaced by createProjection for selection patterns and derived stores.
createComputed
Removed — use createEffect (split model), createSignal(fn) (writable derived), or createMemo.
createMutable
Moved to @solidjs/signals. Not available in solid-js/store.
catchError / onError
Removed — use the <Errored> component for error boundaries.
startTransition / useTransition
Removed — transitions are built-in in Solid 2.0. Use isPending() and <Loading> for transition indicators.
indexArray
Removed — use <For keyed={false}> instead.
Quick rename/removal map
| 1.x | 2.0 | Type |
|---|---|---|
Suspense | Loading | Renamed |
ErrorBoundary | Errored | Renamed |
mergeProps | merge | Renamed |
splitProps | omit | Renamed |
unwrap | snapshot | Renamed |
onMount | onSettled | Renamed |
classList | class (object/array) | Merged |
createResource | async createMemo / createStore(fn) | Replaced |
createSelector | createProjection | Replaced |
createComputed | createEffect / createSignal(fn) / createMemo | Replaced |
batch | Default behavior / flush() | Removed |
on | Split effects | Removed |
Index | <For keyed={false}> | Removed |
catchError / onError | <Errored> | Removed |
startTransition / useTransition | Built-in transitions | Removed |
attr: / bool: / oncapture: | Standard attributes | Removed |
use: directives | ref factories | Removed |
Context.Provider | Context as component | Removed |
createMutable | @solidjs/signals | Moved |