Guides

Migrating to Solid 2.0

Edit this page

This 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: createEffect uses a split model (compute → apply). Cleanup is returned from the apply function.
  • Lifecycle: onMount is replaced by onSettled, which can return cleanup.
  • Async UI: Use <Loading> for first readiness; use isPending() for "refreshing…" indicators.
  • Data fetching: createResource is removed. Use async createMemo or createStore(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() replaces unwrap().
  • DOM: use: directives are removed. Use ref directive factories.
  • Helpers: mergePropsmerge, splitPropsomit.
  • Components: SuspenseLoading, ErrorBoundaryErrored.
  • 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 synchronously
flush()

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 function
createEffect(() => {
document.title = name()
})
// 2.0 — split: compute → apply
createEffect(
() => name(),
(value) => {
document.title = value
}
)

Cleanup is returned from the apply function instead of using onCleanup:

// 1.x
createEffect(() => {
const id = setInterval(() => console.log(name()), 1000)
onCleanup(() => clearInterval(id))
})
// 2.0
createEffect(
() => name(),
(value) => {
const id = setInterval(() => console.log(value), 1000)
return () => clearInterval(id)
}
)

Lifecycle: onMountonSettled

onSettled replaces onMount. It runs after all async descendants have resolved and can return cleanup:

// 1.x
onMount(() => {
measureLayout()
})
// 2.0
onSettled(() => {
measureLayout()
const onResize = () => measureLayout()
window.addEventListener("resize", onResize)
return () => window.removeEventListener("resize", onResize)
})

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.0
function Bad(props) {
const n = props.count
return <div>{n}</div>
}
// ✅ read inside JSX or a reactive scope
function Good(props) {
return <div>{props.count}</div>
}

Destructuring props in the argument list also triggers the warning:

// ❌ warns
function Bad({ title }) {
return <h1>{title}</h1>
}
// ✅ keep the props object
function 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 memo
createMemo(() => setDoubled(count() * 2))
// ✅ derive instead of writing back
const 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.x
const [user] = createResource(id, fetchUser)
// 2.0
const 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" })

SuspenseLoading, ErrorBoundaryErrored

// 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 initial Loading fallback.
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.x
const [data, { mutate, refetch }] = createResource(fetchData)
startTransition(() => mutate(optimistic))
await saveToServer()
refetch()
// 2.0
const 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 arguments
setStore("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"))

unwrapsnapshot

// 1.x
import { unwrap } from "solid-js/store"
const plain = unwrap(store)
// 2.0
import { 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

mergePropsmerge

// 1.x
import { mergeProps } from "solid-js"
const merged = mergeProps(defaults, overrides)
// 2.0
import { merge } from "solid-js"
const merged = merge(defaults, overrides)

splitPropsomit

// 1.x
import { splitProps } from "solid-js"
const [local, rest] = splitProps(props, ["class", "style"])
// 2.0
import { 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()} />

classListclass

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.x
const Theme = createContext("light")
<Theme.Provider value="dark">{props.children}</Theme.Provider>
// 2.0
const 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.x2.0Type
SuspenseLoadingRenamed
ErrorBoundaryErroredRenamed
mergePropsmergeRenamed
splitPropsomitRenamed
unwrapsnapshotRenamed
onMountonSettledRenamed
classListclass (object/array)Merged
createResourceasync createMemo / createStore(fn)Replaced
createSelectorcreateProjectionReplaced
createComputedcreateEffect / createSignal(fn) / createMemoReplaced
batchDefault behavior / flush()Removed
onSplit effectsRemoved
Index<For keyed={false}>Removed
catchError / onError<Errored>Removed
startTransition / useTransitionBuilt-in transitionsRemoved
attr: / bool: / oncapture:Standard attributesRemoved
use: directivesref factoriesRemoved
Context.ProviderContext as componentRemoved
createMutable@solidjs/signalsMoved
Report an issue with this page