From Solid 1.x
Solid 2 changes the execution model as well as the API names. Plan the migration as an application-level upgrade, then convert one subsystem at a time.
Migration order and compatibility boundary
Upgrade the runtime, renderer, JSX compiler, and framework integrations together.
Solid 2 does not provide the old solid-js/web or solid-js/store package exports, and @solidjs/web requires Solid 2 as a peer.
A library that imports removed paths or depends on Solid 1 effect, store, or resource behavior needs a Solid 2-compatible release.
Use this order:
- Create a migration branch and record the passing Solid 1 test, client-build, and server-render baselines.
- Upgrade
solid-js, the renderer package, the Vite plugin (@solidjs/vite-pluginreplacesvite-plugin-solid), and framework adapters as one dependency set. - Move imports and JSX types until TypeScript and the production build resolve the new package boundary.
- Convert setter timing, effects, lifecycle code, and writes inside reactive scopes.
- Convert stores and store utilities.
- Replace resources, removed transition APIs, mutations, and async boundaries.
- Update client rendering, server-side rendering (SSR), hydration, and host integrations.
- Run tests in development mode to expose Solid 2 diagnostics, then verify production client and server builds.
Do not use a passing type check as proof that an integration is compatible. Compiler transforms, root event delegation, hydration IDs, and async scheduling are runtime contracts.
Move packages and JSX ownership
Web rendering and web JSX types moved out of solid-js.
Store APIs moved in the other direction and are now exported from solid-js.
// Solid 1import { createStore, reconcile } from "solid-js/store";import { hydrate, render } from "solid-js/web";
// Solid 2import { createStore, reconcile } from "solid-js";import { hydrate, render } from "@solidjs/web";Update web projects to use the renderer as the JSX type owner:
{ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "@solidjs/web" }}Import DOM-specific JSX and ComponentProps types from @solidjs/web.
Use renderer-neutral types such as Component and Element from solid-js when an API does not depend on DOM JSX.
The alternate renderers also moved:
solid-js/hmoved to@solidjs/h.solid-js/htmlmoved to@solidjs/html.solid-js/universalmoved to@solidjs/universal.solid-js/jsx-runtimeandsolid-js/jsx-dev-runtimemoved to renderer-owned entries such as@solidjs/web/jsx-runtime.
Search source files, generated code, test configuration, package export maps, and declaration files for the old paths.
Convert signals and staged writes
The getter and setter pair from createSignal remains.
Setter visibility changed.
Solid 2 stages ordinary writes and commits the reactive queue on the next microtask.
A read immediately after a setter returns the last committed value.
const [count, setCount] = createSignal(0);
setCount(1);count(); // 0
flush();count(); // 1Most event handlers need no explicit flush.
Add flush() only at an imperative boundary that must observe updated reactive state or DOM before the next microtask.
Tests are a common example.
flush(fn) runs the writes in a synchronous flush scope and drains the queue before it returns.
Remove batch.
Consecutive writes already share the default microtask batch.
Replacing each batch call with flush would change deferred work into synchronous work.
Development mode also rejects writes in ordinary owned scopes such as component bodies and memo computations.
Replace write-back derivations with createMemo, and invoke setters from event handlers, actions, or effect effect functions.
Reserve ownedWrite: true for an internal signal that intentionally accepts in-scope writes.
Solid 2 warns when a component body reads a reactive value at top level because the read is outside a tracking scope.
Keep reactive props and store properties inside JSX expressions or computations.
Use untrack only for an intentional one-time read.
// Solid 1 code that loses reactivity in Solid 2function Heading({ title }: { title: string }) { return <h1>{title}</h1>;}
// Solid 2function Heading(props: { title: string }) { return <h1>{props.title}</h1>;}Function-form createSignal(fn) is new.
It creates a writable derived signal and can replace a derivation that previously wrote back into another signal.
Split effects into compute and effect phases
createEffect now separates dependency tracking from the side effect:
// Solid 1createEffect(() => { document.title = title();});
// Solid 2createEffect( () => title(), (value) => { document.title = value; });The compute function tracks reactive reads and must remain free of application writes. The effect function is untracked, may perform imperative work, and may return a cleanup function. Extract store properties in the compute phase instead of passing a store proxy through and reading it in the untracked effect phase.
createEffect( () => ({ name: user.name, role: user.role }), (value) => sendAnalytics(value.name, value.role));Return effect cleanup from the effect function:
createEffect( () => roomId(), (id) => { const connection = connect(id); return () => connection.close(); });The Solid 1 initialValue argument is removed from createEffect and createMemo.
The compute function receives prev, which is undefined on its first run.
Use a default parameter when the computation needs a first previous value.
The second createMemo argument is now its options object.
Replace common lifecycle and effect helpers as follows:
- Replace
on(...)with the compute function and use the effectdeferoption when needed. - Replace
onMountwithonSettledand return cleanup from its callback. - Replace effect-local
onCleanupcalls with cleanup returned from the effect function. - Use
createTrackedEffectonly when one tracked callback is required. It andonSettledcannot create nested primitives. - Replace
catchErrororonErrorwith anErroredboundary or the effect bundleerrorcallback.
Convert stores to draft setters
createStore still returns a read-only store and its setter.
The setter now receives a mutable draft.
This preserves the property-level model from Solid 1 while making draft mutation the default update form.
// Solid 1setState("user", "address", "city", "Paris");
// Solid 2setState((draft) => { draft.user.address.city = "Paris";});Delete produce(...) wrappers and pass the producer body directly to the setter.
Replace createMutable and modifyMutable with createStore and explicit draft setters.
Store writes follow the same staged-write timing as signals.
The setter takes no path arguments.
A Solid 1 path setter becomes a draft write to the same path; ranges and filters become ordinary loops, and setting undefined to remove a key becomes delete:
// Solid 1: setState("items", { from: 1, to: 4, by: 2 }, 99)setState((draft) => { for (let i = 1; i <= 4; i += 2) draft.items[i] = 99;});
// Solid 1: setState("todos", (todo) => todo.done, "archived", true)setState((draft) => { for (const todo of draft.todos) if (todo.done) todo.archived = true;});
// Solid 1: setState("nickname", undefined)setState((draft) => { delete draft.nickname;});reconcile now runs against the selected draft:
// Solid 1setState("todos", reconcile(serverTodos));
// Solid 2setState((draft) => { reconcile(serverTodos, "id")(draft.todos);});Review these utility changes:
- Replace
unwrap(store)withsnapshot(store)when serialization or external code needs a plain non-reactive value. - Replace
mergePropswithmerge.undefinedis an explicit overriding value inmerge. - Replace
splitProps(props, ["a", "b"])withomit(props, "a", "b"). - Replace selector-shaped state with
createProjectionor function-formcreateStore.
Use deep(store) in an effect compute phase when the effect must subscribe to every nested property.
Use snapshot(store) when the effect needs the current plain value without subscribing to the store.
Replace resources with async computations
Solid 2 computations accept promises and async iterables.
Replace a basic createResource with an async memo:
// Solid 1const [user] = createResource(userId, fetchUser);
// Solid 2const user = createMemo(() => fetchUser(userId()));Place reads that can be initially unresolved under Loading:
<Loading fallback={<UserSkeleton />}> <UserProfile user={user()} /></Loading>This is the primary first-load model. The boundary owns branch readiness and renders its fallback until the required reads settle. After content has rendered, a later update is held and keeps the committed content visible.
A write to an input of an async computation is held until that computation has its next answer, and everything else in the same update waits with it. Solid 1 resources and effect-based fetching never did this; each consumer updated on its own schedule. An application that converts all of its fetching at once inherits the new behavior everywhere, and a shared input such as a period selector then waits for the slowest fetch that reads it. Data fetching from Solid 1 shows how to keep each pattern working unchanged first, then convert one fetch at a time and choose the refetch behavior per fetch.
The properties and actions on a Solid 1 resource become separate operations:
- Replace
resource.loadingwith aLoadingboundary for initial readiness andisPending(() => resource())for an in-flight changed answer. - Replace
resource.errorwith anErroredboundary or an effecterrorcallback. - Replace
refetch()withrefresh(resource). - Replace
mutate()with an action andcreateOptimisticorcreateOptimisticStore. - Replace
resource.latestwithlatest(resource)when imperative code must inspect an in-flight value.
A bare refresh() re-asks the same question without making isPending true.
If the refresh must present as pending, call affects(target) in the surrounding action before refresh(target).
Use a mutation-specific optimistic flag when the UI needs to show the progress of the mutation itself.
loadingValue and the store option seedLoadingValue declare a placeholder that answers for the source before its first result.
Use them when provisional data should render through the same UI as the final data, or when a Solid 1 undefined check must stay in the JSX.
The declared value prevents first-flight suspension and keeps isPending false until the first real answer arrives.
Use Loading when provisional data cannot truthfully have the final value's shape.
Convert mutations and remove transition APIs
Remove startTransition and useTransition.
Solid 2 coordinates async computations through the graph.
Use Loading for unresolved branches, isPending for an unrevealed changed answer, and optimistic primitives for tentative mutation state.
action is for imperative workflows whose writes cross an async gap.
An action is a generator or async generator and returns a promise.
Writes to ordinary signals and stores remain held by the transaction, while optimistic writes are visible and revert to their derived or base value when the transaction settles.
const [todos, setTodos] = createOptimisticStore(fetchTodos, []);
const addTodo = action(function* (todo: Todo) { setTodos((draft) => { draft.push(todo); }); yield saveTodo(todo); refresh(todos);});Yield promises to preserve the action transaction.
If an async generator uses await for a typed result, add a bare yield before later writes to re-enter the transaction.
Do not call flush() inside an action because it drains the transaction step.
Invoke actions from event handlers or another imperative scope, not from a component or computation body.
Read more about the unified model in Async reactivity.
Update boundaries, control flow, and JSX
Apply these direct UI migrations:
- Replace
SuspensewithLoading. - Replace
ErrorBoundarywithErrored. Its function fallback receives an error accessor, so read the error witherror(). - Replace
SuspenseListwithReveal. - Replace
IndexwithFor keyed={false}. The item is an accessor and the index is a stable number in this mode. - Replace
Context.Providerwith the context component:<Theme value={value}>. - Replace
<Dynamic component={source} {...props} />with a component fromdynamic():const Comp = dynamic(() => source), then<Comp {...props} />.Dynamicis deprecated. Create the component once, outside the JSX, and render it wherever it is needed; a falsy source renders nothing, as before. - Replace direct
createDynamic(source, props)calls withdynamic(source). - Use
dynamicComponentfromsolid-jsinstead when the source only ever answers with a component, never a tag name; it does not keep the element runtime in the bundle.
Update removed JSX forms:
- Replace
classListwith the object or array form ofclass. - Replace
use:directivewith arefcallback or directive factory, and use arefarray to compose callbacks. - Replace
on:andoncapture:with camel-case Solid event props. Use arefcallback andaddEventListenerwhen native listener options are required. - Rename lowercase handlers such as
onclick={fn}toonClick. Onlyonfollowed by an uppercase letter is an event handler; a lowercaseonclickor a leftoveron:clickcompiles to a plain attribute without an error, and development warns[LOWERCASE_EVENT_ATTRIBUTE]when it receives a function. - Replace
attr:andbool:with standard attributes and boolean presence. - Remove
/*@once*/. Keep values reactive, use DOM default properties such asdefaultValuefor initial state, or take a narrow JavaScript snapshot withuntrack.
Rendering into nested roots, ShadowRoot instances, or portals needs integration testing.
Solid 2 scopes delegated events to each render root and disposes those listeners with the root.
Remove calls to the old document-global clearDelegatedEvents API.
Update client rendering and SSR
Import render, hydrate, and all server render functions from @solidjs/web.
Continue to retain and call the dispose function returned by render.
Choose the SSR entry point by the required result:
renderToStringis synchronous and rendersLoadingfallbacks for unresolved reads.renderToStreamemits the shell and then resolved async fragments.- Replace
renderToStringAsyncwithawait renderToStream(() => <App />)when a fully settled HTML string is required. - Use exactly one stream consumer:
pipe,pipeTo, orreadable. Thereadablestream containsUint8Arraychunks and can be passed to a webResponse.
Review the ssrSource and deferStream options on every migrated resource.
The default ssrSource: "server" adopts the serialized server result without repeating the initial client computation.
Use "hybrid" or "client" only when the computation has verified client-specific behavior.
Code that owns the HTTP exchange is an integration boundary.
Solid 2 exposes request events, response-head helpers, server functions, and stream response utilities through @solidjs/web, but routing, middleware policy, sessions, and deployment adapters still need framework-specific migration work.
Do not infer replacements for those APIs from similar names.
Follow the integration's Solid 2 guide and verify its request, response, hydration, and serialization tests.
Replace removed APIs by intent
The following list covers common application APIs and is not a complete export-diff table. Choose replacements by behavior:
createComputed: usecreateMemofor a derived value, splitcreateEffectfor a side effect, or function-formcreateSignalfor writable derived state.batch: rely on default batching; useflushonly for synchronous observation.on: put dependencies in the effect compute phase.onMount: useonSettled.onErrorandcatchError: useErroredor an effecterrorcallback.createResource: use async computations and boundaries.createMutableandmodifyMutable: usecreateStoreand draft setters.produce: pass its draft callback directly to the store setter.createSelector: usecreateProjectionor function-formcreateStore.fromandobservable: use async iterables for inbound streams or a split effect for outbound notifications. A standard Observable adapter has no direct core replacement.createDeferred: move the scheduling policy outside Solid.indexArray: usemapArraywith{ keyed: false }.resetErrorBoundaries: remove it; error boundaries recover through their current graph state or explicit reset callback.enableSchedulingandwriteSignal: remove internal or obsolete usage.
Audit undocumented imports separately. An export that existed for compiler, renderer, devtools, or metaframework integration may have no application-level replacement. Treat those imports as host-integration work and confirm them against the owning package source.
Update tests
Update test compilation to use the renderer's JSX runtime.
For web component tests, resolve the browser and development package conditions so tests exercise @solidjs/web rather than the server entry.
Keep Testing Library cleanup or call render disposers explicitly.
Account for staged writes in unit tests:
setCount(2);flush();expect(count()).toBe(2);Flush after effect creation before asserting the effect function.
Flush after setters before asserting reactive values, effects, or DOM.
Prefer await user.click(...) for user-level tests because event tooling also drains its event sequence.
Use resolve(() => value()) when a test must wait for a reactive expression to settle.
Await action calls before asserting their final committed or reverted state.
Test the initial Loading fallback, the settled content, the held-update pending state, and error boundary as separate states.
Run migration tests in development mode.
Fix top-level reactive-read warnings, writes from owned scopes, actions called from computations, and async reads outside a Loading boundary instead of suppressing the diagnostics.
Then run the same client, SSR, streaming, and hydration paths in production mode.
See the Testing guide for the current test environment split.
Final verification
Before merging the migration:
- Search for old package paths and removed symbols.
- Run TypeScript, lint, and the production client build.
- Run unit and component tests with explicit flush points.
- Exercise initial load, a changed-input update, manual refresh, successful mutation, failed mutation, and boundary recovery.
- Compare SSR shell output, streamed completion, hydration, and event behavior with the recorded Solid 1 baseline.
- Verify each third-party primitive, renderer, router, and metaframework package against an explicit Solid 2-compatible release.
Next steps
- Data fetching from Solid 1: convert resource and effect-based fetching one fetch at a time and choose the refetch behavior for each.
- From SolidStart, From Solid Router, and From Solid Meta: the framework-level migrations that build on this one.
- Async reactivity: the model behind held updates,
Loading,isPending, and actions.