This is a short, practical guide for migrating from Solid 1.x to Solid 2.0’s APIs. It focuses on the changes you’ll hit most often and shows “before/after” examples.
- Imports: some 1.x subpath imports moved to
@solidjs/*packages (and store helpers moved intosolid-js). - Batching/reads: setters don’t immediately change what reads return; values become visible after the microtask batch flushes (or via
flush()). - Effects:
createEffectis split (compute → apply). Cleanup is usually “return a cleanup function”. - Lifecycle:
onMountis replaced byonSettled(and it can return cleanup). - Async UI: use
<Loading>for first readiness; useisPending(() => expr)for “refreshing…” indicators. - Lists:
Indexis gone; use<For keyed={false}>.Forchildren receive accessors (item()/i()). - Stores: prefer draft-first setters;
storePath(...)exists as an opt-in helper for the old path-style ergonomics. - Plain values:
snapshot(store)replacesunwrap(store)when you need a plain non-reactive value. - DOM:
use:directives are removed; userefdirective factories (and array refs). - Helpers:
mergeProps→merge,splitProps→omit.
In Solid 2.0 beta, the DOM/web runtime is its own package, and some “subpath imports” from 1.x are gone.
// 1.x (DOM runtime)
import { render, hydrate } from "solid-js/web";
// 2.0 beta
import { render, hydrate } from "@solidjs/web";// 1.x (stores)
import { createStore } from "solid-js/store";
// 2.0 beta (stores are exported from solid-js)
import { createStore, reconcile, snapshot, storePath } from "solid-js";// 1.x (hyperscript / alternate JSX factory)
import h from "solid-js/h";
// 2.0 beta
import h from "@solidjs/h";// 1.x (tagged-template HTML)
import html from "solid-js/html";
// 2.0 beta
import html from "@solidjs/html";// 1.x (custom renderers)
import { createRenderer } from "solid-js/universal";
// 2.0 beta
import { createRenderer } from "@solidjs/universal";In Solid 2.0, updates are batched by default (microtasks). A key behavioral change is that setters don’t immediately update what reads return — the new value becomes visible when the batch is flushed (next microtask), or immediately if you call flush().
const [count, setCount] = createSignal(0);
setCount(1);
count(); // still 0
flush();
count(); // now 1Use flush() sparingly (it forces the system to “catch up now”). It’s most useful in tests, or in rare imperative code where you truly need a synchronous “settled now” point.
Solid 2.0 splits effects into two phases:
- a compute function that runs in the reactive tracking phase and returns a value
- an apply function that receives that value and performs side effects (and can return cleanup)
// 1.x (single function effect)
createEffect(() => {
el().title = name();
});
// 2.0 (split effect: compute -> apply)
createEffect(
() => name(),
value => {
el().title = value;
}
);Cleanup usually lives on the apply side now:
// 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);
}
);If you used onMount, the closest replacement is onSettled (and it can also return cleanup):
// 1.x
onMount(() => {
measureLayout();
});
// 2.0
onSettled(() => {
measureLayout();
const onResize = () => measureLayout();
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
});These are dev-only warnings meant to catch subtle bugs earlier.
In 2.0, reading reactive values at the top level of a component body (including destructuring props) will warn. The fix is usually to move the read into a reactive scope (createMemo/createEffect) or make the intent explicit with untrack.
// ❌ 2.0 warns (top-level reactive read)
function Bad(props) {
const n = props.count;
return <div>{n}</div>;
}
// ✅ read inside JSX/expression
function Ok(props) {
return <div>{props.count}</div>;
}// ❌ 2.0 warns (common: destructuring in args)
function BadArgs({ title }) {
return <h1>{title}</h1>;
}
// ✅ keep props object, or destructure inside a memo/effect
function OkArgs(props) {
return <h1>{props.title}</h1>;
}Writing to signals/stores inside a reactive scope will warn. Usually you want:
- derive values with
createMemo(no write-back) - write in event handlers / actions
- return cleanup from effect apply functions (instead of writing during tracking)
// ❌ warns: writing from inside a memo
createMemo(() => setDoubled(count() * 2));
// ✅ derive instead of writing back
const doubled = createMemo(() => count() * 2);If you truly have an internal signal that needs to be written from within owned scope (not app state), opt in narrowly with pureWrite: true.
// 1.x
<Suspense fallback={<Spinner />}>
<Profile />
</Suspense>
// 2.0
<Loading fallback={<Spinner />}>
<Profile />
</Loading>// 1.x
const [user] = createResource(id, fetchUser);
// 2.0
const user = createMemo(() => fetchUser(id()));<Loading fallback={<Spinner />}>
<Profile user={user()} />
</Loading>Loading: initial “not ready yet” UI boundary.isPending: “stale while revalidating” indicator; false during the initialLoadingfallback.
const listPending = () => isPending(() => users() || posts());
<>
<Show when={listPending()}>{/* subtle "refreshing…" indicator */}</Show>
<Loading fallback={<Spinner />}>
<List users={users()} posts={posts()} />
</Loading>
</>const latestId = () => latest(id);// After a server write, explicitly recompute a derived read:
refresh(storeOrProjection);
// Or re-run a read tree:
refresh(() => query.user(id()));In 1.x, mutations often ended up as “call an async function, flip some flags, then manually refetch”. In 2.0, the recommended shape is:
- wrap mutations in
action(...) - use
createOptimistic/createOptimisticStorefor optimistic UI - call
refresh(...)at the end to recompute derived reads
const [todos] = createStore(() => api.getTodos(), { list: [] });
const [optimisticTodos, setOptimisticTodos] = createOptimisticStore({ list: [] });
const addTodo = action(function* (todo) {
// optimistic UI
setOptimisticTodos(s => s.list.push(todo));
// server write
yield api.addTodo(todo);
// recompute reads derived from the source-of-truth
refresh(todos);
});// 2.0 preferred: produce-style draft updates
setStore(s => {
s.user.address.city = "Paris";
});
// Optional compatibility: old “path argument” ergonomics via storePath
setStore(storePath("user", "address", "city", "Paris"));const plain = snapshot(store);
JSON.stringify(plain);// 1.x
const merged = mergeProps(defaults, overrides);
// 2.0
const merged = merge(defaults, overrides);One behavioral gotcha: undefined is treated as a real value (it overrides), not “skip this key”.
const merged = merge({ a: 1, b: 2 }, { b: undefined });
// merged.b is undefined// 1.x
const [local, rest] = splitProps(props, ["class", "style"]);
// 2.0
const rest = omit(props, "class", "style");createSignal(fn) creates a writable derived signal (think “writable memo”):
const [count, setCount] = createSignal(0);
const [doubled] = createSignal(() => count() * 2);createStore(fn, initial) creates a derived store using the familiar createStore API:
const [items] = createStore(() => api.listItems(), []);
const [cache] = createStore(
draft => {
draft.total = items().length;
},
{ total: 0 }
);If you used Index, it’s now For with keyed={false}.
The breaking bit: the For child function receives accessors for both the item and the index, so you’ll write item() / i() (not item / i).
// 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>This isn’t just For. A few control-flow APIs pass accessors into function children so the value is always safe to read:
<Show when={user()} fallback={<Login />}>
{u => <Profile user={u()} />}
</Show>
<Switch>
<Match when={route() === "profile"}>{() => <Profile />}</Match>
</Switch>Solid 2.0 aims to be more “what you write is what the platform sees”:
- built-in attributes are treated as attributes (not magically mapped properties), and are generally lowercase
- boolean attributes are presence/absence (
muted={true}adds it,muted={false}removes it) attr:andbool:namespaces are removed (you typically don’t need them)
<video muted={true} />
<video muted={false} />
// When the platform really wants a string:
<some-element enabled="true" />Also, oncapture: is removed.
// 1.x
<button use:tooltip={{ content: "Save" }} />
// 2.0
<button ref={tooltip({ content: "Save" })} />
<button ref={[autofocus, tooltip({ content: "Save" })]} />Two-phase directive factories are recommended (owned setup → unowned apply):
function titleDirective(source) {
// Setup phase (owned): create primitives/subscriptions here.
// Avoid imperative DOM mutation at top level.
let el;
createEffect(source, value => {
if (el) el.title = value;
});
// Apply phase (unowned): DOM writes happen here.
// No new primitives should be created in this callback.
return nextEl => {
el = nextEl;
};
}// 1.x
<div class="card" classList={{ active: isActive(), disabled: isDisabled() }} />
// 2.0
<div class={["card", { active: isActive(), disabled: isDisabled() }]} />// 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>solid-js/web→@solidjs/websolid-js/store→solid-jssolid-js/h→@solidjs/hsolid-js/html→@solidjs/htmlsolid-js/universal→@solidjs/universalSuspense→LoadingErrorBoundary→ErroredmergeProps→mergesplitProps→omitcreateSelector→createProjection/createStore(fn)unwrap→snapshotclassList→classmergeProps/splitProps→merge/omitcreateResourceremoved → async computations +LoadingstartTransition/useTransitionremoved → built-in transitions +isPending/Loading+ optimistic APIsuse:directives removed →refdirective factoriesattr:/bool:removed → standard attribute behavioroncapture:removedonMount→onSettled