| name | solid-js-2.x-migration |
| description | Guidance for migrating an existing SolidJS 1.x codebase to SolidJS 2.x beta or next, including identifying old APIs, rewriting code to the new semantics, sequencing changes safely, and validating the migration with warnings and tests. Use this whenever the user asks to upgrade, port, convert, or migrate SolidJS code from 1.x to 2.x, especially when they mention renamed imports, removed APIs like Index or createResource, new effect behavior, Loading or Errored, store API changes, failing tests after upgrade, or ecosystem breakage during the move to Solid 2.x beta or next. |
SolidJS 2.x Migration
Use this skill when the user already has Solid 1.x code and wants to move it to Solid 2.x beta or next.
This skill is migration-oriented. Its job is not just to explain the new APIs, but to convert old code safely and preserve behavior where possible.
If the user only wants a narrow explanation of a Solid 2.x primitive or warning and is not actively upgrading existing code, prefer the Solid 2.x API skill instead.
Goals
- Identify likely 1.x patterns quickly.
- Apply the highest-confidence migrations first.
- Avoid shallow renames that miss semantic changes.
- Use tests and dev warnings to validate the migration.
- Be honest about beta-era ecosystem gaps and package workarounds.
Scope boundary
- Use this skill for upgrades, ports, conversions, staged migration plans, and debugging breakage that appeared during or after the upgrade.
- Keep the focus on moving existing code safely, not on giving a general tour of the 2.x API surface.
- When a task is primarily Start-specific, use the SolidStart skill alongside this one rather than assuming the framework boundary from generic Solid guidance.
Migration mindset
Treat the migration as two kinds of work:
- mechanical changes that are mostly straightforward
- semantic changes that need careful reasoning and validation
Do not present everything as equally safe. Call out where a rewrite is mechanical versus where behavior may change.
Do not invent replacement APIs that are not part of the Solid 2 migration guidance in this skill. If a candidate rewrite depends on neighboring framework helpers or uncertain beta exports, say that explicitly and keep the migration on the highest-confidence Solid core path.
How to use this skill
Read this skill in passes instead of treating every section as equally important.
- Start with
Migration mindset, Recommended migration workflow, and High-confidence mechanical migrations.
- Use
Canonical before and after rewrites for the first implementation pass.
- Use
Semantics that need careful migration when behavior, warnings, tests, or lifetime issues are involved.
- Use
Task-focused migration guidance and Common migration mistakes to catch for review, triage, and planning.
Recommended migration workflow
- Inspect package versions, imports, and the current usage of 1.x APIs.
- Make import and package-boundary changes.
- Migrate removed or renamed APIs.
- Migrate behavior-sensitive reactivity patterns.
- Run tests or at least read dev warnings carefully.
- Fix remaining ecosystem or tooling issues.
High-confidence mechanical migrations
Apply these first when present:
solid-js/web -> @solidjs/web
solid-js/store imports -> solid-js
solid-js/h -> @solidjs/h
solid-js/html -> @solidjs/html
solid-js/universal -> @solidjs/universal
Suspense -> Loading
ErrorBoundary -> Errored
Index -> For keyed={false}
createSelector -> createProjection or function-form createStore
unwrap -> snapshot
mergeProps -> merge
splitProps -> omit
classList -> class
use: directives -> ref directive factories
attr: and bool: -> standard attribute behavior
oncapture: removed
onMount -> onSettled
Even for mechanical migrations, quickly verify surrounding semantics instead of blindly rewriting.
Also remember the non-trivial removals:
createResource is not a simple rename; migrate toward async computations plus Loading
startTransition and useTransition go away; express pending and transition UX with built-in transitions, isPending, Loading, and optimistic primitives
createComputed is removed; replace it based on intent rather than by pattern-matching syntax
Canonical before and after rewrites
Keep rewrites terse and behavior-aware.
Imports
import { render } from "solid-js/web";
import { createStore } from "solid-js/store";
import { render } from "@solidjs/web";
import { createStore } from "solid-js";
Index to For
<Index each={items()}>{(item, i) => <Row item={item()} index={i} />}</Index>
<For each={items()} keyed={false}>{(item, i) => <Row item={item()} index={i()} />}</For>
The migration is not just renaming the component. The index is now also an accessor.
Suspense to Loading
<Suspense fallback={<Spinner />}>
<Page />
</Suspense>
<Loading fallback={<Spinner />}>
<Page />
</Loading>
ErrorBoundary to Errored
<ErrorBoundary fallback={(err, reset) => <Fallback err={err} reset={reset} />}>
<Page />
</ErrorBoundary>
<Errored fallback={(err, reset) => <Fallback err={err} reset={reset} />}>
<Page />
</Errored>
onMount to onSettled
onMount(() => {
measureLayout();
});
onSettled(() => {
measureLayout();
});
Treat this as a lifecycle semantic review, not a blind search-and-replace, especially when old mount logic created nested primitives or assumed immediate post-setter reads.
Single-callback effect to split effect
createEffect(() => {
document.title = title();
});
createEffect(
() => title(),
value => {
document.title = value;
}
);
Cleanup moves to apply side
createEffect(() => {
const id = setInterval(() => console.log(name()), 1000);
onCleanup(() => clearInterval(id));
});
createEffect(
() => name(),
value => {
const id = setInterval(() => console.log(value), 1000);
return () => clearInterval(id);
}
);
Top-level prop read fix
function Title(props) {
const t = props.title;
return <h1>{t}</h1>;
}
function Title(props) {
return <h1>{props.title}</h1>;
}
Store helper changes
const plain = unwrap(store);
const merged = mergeProps(defaults, overrides);
const [local, rest] = splitProps(props, ["class"]);
const plain = snapshot(store);
const merged = merge(defaults, overrides);
const rest = omit(props, "class");
Important semantic change:
- with
merge, undefined is an explicit override value, not a skipped key
createSelector to projection
const isSelected = createSelector(selectedId);
const selected = createProjection((draft) => {
const id = selectedId();
draft[id] = true;
if (draft._prev != null) delete draft[draft._prev];
draft._prev = id;
}, {});
If the old code used selector-like patterns, think in terms of projection-oriented derived state rather than hunting for a one-to-one selector replacement.
Draft-first store update
setStore("user", "address", "city", "Paris");
setStore(s => {
s.user.address.city = "Paris";
});
setStore(storePath("user", "address", "city", "Paris"));
Setter return values also matter now:
setStore(s => {
return { ...s, list: [] };
});
That is a top-level shallow replacement or diff, not the same thing as mutating the draft in place.
Semantics that need careful migration
Batching and test behavior
In 2.x, reads do not immediately observe just-written signal values. If tests or imperative code read state immediately after setters, they may now need a flush point.
Use flush() narrowly, especially in tests or imperative DOM code. Do not scatter it everywhere as a generic fix.
const [count, setCount] = createSignal(0);
setCount(1);
flush();
expect(count()).toBe(1);
The practical insight from early migrations: this tends to show up in unit tests far more than end-to-end tests.
For imperative DOM reads, flush() is also the replacement for many old batch(...) expectations.
createEffect
Migrate from single-callback effects to compute/apply form when the effect depends on reactive reads and performs side effects.
When migrating:
- move tracked reads into the compute half
- move side effects into the apply half
- return cleanup from the apply half
If the old effect was really deriving state, replace it with a memo or derived primitive instead of translating it directly.
This is one of the biggest migration traps. Do not reward old effect-heavy code by preserving the shape if the real issue is that the code should be declarative.
onMount to onSettled
Replace onMount with onSettled, but verify intent. Some code assumed old lifecycle timing or mixed mount work with nested primitive creation. Treat these cases carefully.
Context.Provider to context-as-provider
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
<ThemeContext value="dark">
<Page />
</ThemeContext>
createComputed replacement
Choose the replacement by intent:
- side effects -> split
createEffect
- derived writable state -> function-form
createSignal or createStore
- readonly derivation ->
createMemo
createComputed(() => {
setValue(props.input);
});
const [value, setValue] = createSignal(() => props.input);
If the old createComputed was really doing side effects, migrate to split createEffect instead.
Ownership and root lifetime
In 2.x, roots created under an owner are owned by that parent by default. If old code depended on effectively detached lifetime, make the detachment explicit.
const singleton = runWithOwner(null, () => {
const [value, setValue] = createSignal(0);
return { value, setValue };
});
Top-level reads and prop destructuring
2.x warns on top-level reactive reads. Migration often requires:
- moving reads into JSX, memos, or effect compute functions
- avoiding destructuring reactive props at component top level
- using
untrack only when a one-time read is truly intentional
This also applies to control-flow callback bodies. If old code read reactive values in Show or For callback bodies before returning JSX, move those reads into JSX expressions or a proper reactive scope.
Writes in owned scope
Old patterns that wrote state from memos or tracked scopes should usually be redesigned rather than patched. Prefer derived state or event-driven writes.
pureWrite: true is for narrow internal cases, not for silencing application-state warnings during migration.
Async data
Do not just rename Suspense to Loading and stop there. If the app relied on createResource, migrate toward async computations plus Loading, pending indicators, and explicit refresh patterns.
Also distinguish initial loading from refresh state:
Loading for not-ready-yet
isPending for updating-again
latest when stale-while-refreshing output should remain visible
Reasonable 2.x migration targets include:
createMemo(async () => ...) for async derived values
createStore(fn, initial) or createProjection(...) for async derived collections or keyed reconciled results
For selector-heavy or keyed-list code, createProjection(...) is often the more faithful migration target.
const [user] = createResource(id, fetchUser);
const user = createMemo(async () => {
const currentId = id();
return fetchUser(currentId);
});
Then move loading UX to structure:
<Loading fallback={<Spinner />}>
<Profile user={user()} />
</Loading>
Do not recreate resource.loading flags mechanically. If the old UI wanted stale-while-refreshing behavior, model that with isPending(() => expr) and optionally latest(...).
const listPending = () => isPending(() => users() || posts());
If the old app used startTransition or useTransition for mutation UX, move that logic toward action(...), optimistic primitives, and refresh(...).
const [todos] = createStore(() => api.getTodos(), []);
const [optimisticTodos, setOptimisticTodos] = createOptimisticStore(() => todos(), []);
const addTodo = action(function* (todo) {
setOptimisticTodos(list => {
list.push(todo);
});
yield api.addTodo(todo);
refresh(todos);
});
Stores
Prefer draft-first store updates in 2.x. If path-style setters are deeply woven into the codebase, storePath(...) can help stage the migration without rewriting every update immediately.
Use storePath(...) as a bridge, not as the main style to promote once the migration settles.
Ecosystem and tooling caveats
Solid 2.x beta or next migrations may require temporary compatibility work, especially around:
- testing libraries
- package aliases
- SSR bundling
- deduping local Solid versions
- package versions pinned to
next
If a dependency has not caught up, say so explicitly and separate framework migration work from ecosystem breakage.
Useful migration-era insights from real projects:
- some packages may still require aliasing or temporary config surgery
- test tooling may need
solid-js/web aliased to @solidjs/web
- linked local packages may need dedupe settings to avoid multiple Solid copies
next tags and temporary overrides may be the practical path during beta adoption
- development warnings are often your first accurate signal that a migration is semantically wrong, not just syntactically incomplete
- deep observation is no longer something to stumble into casually; if the old code expected "watch everything" behavior, it may need an explicit
deep(store) read
- loading and mutation state are less primitive-specific now, so many old
.loading and transition-wrapper patterns should collapse into Loading, isPending, actions, and optimistic primitives
- old code that accidentally relied on unowned lifetime may start behaving more sanely in 2.x, but singleton-style integrations should now detach explicitly with
runWithOwner(null, ...)
- writes-under-scope and strict-read warnings are usually design feedback, not noise to suppress
Task-focused migration guidance
Planning a staged migration
Load these ideas first:
- separate mechanical changes from semantic rewrites
- change imports and package boundaries early
- use compatibility bridges such as
storePath(...) only when they reduce migration risk
- leave time for warning cleanup and test fixes, not just code rewrites
Migrating component code
Look for these first:
Index to For keyed={false} with accessor-aware callbacks
- top-level prop reads and destructuring that now warn
onMount assumptions that need onSettled
Context.Provider habits
use: directives that need ref factories
Migrating reactivity patterns
Look for these first:
- state derivation done through effects instead of
createMemo
- writes to signals or stores from memos or other tracked scopes
- code that assumes post-setter reads are immediately updated
- nested
createRoot(...) calls that assumed detached lifetime
Migrating async data and mutations
Load these ideas first:
- do not stop at
Suspense -> Loading
- move async reads toward computations plus
Loading
- treat
Loading and isPending as different jobs
- move old transition-style mutation UX toward
action(...), optimistic primitives, and refresh(...)
- keep async reads and async writes as separate migration concerns
Debugging test and tooling breakage
Look for these first:
- stale immediate reads after setters in tests
- aliasing or package-boundary mismatches
- duplicate Solid versions in linked packages or monorepos
- ecosystem lag in test libraries or SSR tooling
- warnings that reveal semantic migration errors rather than bad syntax
Common migration mistakes to catch
| Mistake | Better direction |
|---|
| Treating every migration as a rename pass | Separate mechanical changes from semantic rewrites and say which is which. |
Renaming Suspense to Loading but leaving the old async model intact | Migrate the read model too, not just the boundary component. |
Treating createResource like it has a one-word replacement | Move toward async computations plus Loading, isPending, and refresh(...) where appropriate. |
| Preserving write-back effects just because they existed in 1.x | Replace derivation with createMemo or another derived form when that was the real intent. |
Scattering flush() everywhere after test failures | Use it narrowly where a settled point is genuinely needed. |
Treating storePath(...) as the final 2.x style | Use it as a bridge, then prefer draft-first setters in migrated code. |
Using pureWrite: true to suppress owned-scope warnings in app code | Redesign the state flow instead; reserve pureWrite: true for narrow internal cases. |
Forgetting accessor semantics when replacing Index | In For keyed={false} callbacks, read item() and i() when accessors are provided. |
Assuming nested createRoot(...) stayed detached like before | Make detached lifetime explicit with runWithOwner(null, ...) when it is truly required. |
Reaching for createAsync, router helpers, or Start helpers during core migration | Stay on the highest-confidence Solid core migration path unless the repo proves a framework-specific target. |
| Treating warnings as noise to suppress | Use them as migration feedback about state flow, read placement, and ownership. |
When to pause and ask the user
Ask for confirmation when:
- a change appears behavior-affecting rather than mechanical
- a dependency is not Solid 2.x compatible yet
- there are multiple plausible migration strategies with real tradeoffs
Otherwise, keep moving through the migration.
Output expectations
When migrating code, prefer this structure:
- List the 1.x patterns found.
- Separate mechanical changes from semantic rewrites.
- Apply minimal safe code changes.
- Note any remaining warnings, test failures, or ecosystem blockers.
When possible, include one-line reasoning beside each migration hotspot so the user understands why the rewrite happened.
For code rewrites, keep these guardrails:
- do not switch to unrelated helpers like
createAsync or mount unless the local repo proves they are the right target
- prefer the highest-confidence Solid core migration path from this skill over neighboring framework patterns
- if the exact beta export is uncertain, say so explicitly instead of fabricating a confident rewrite
Example prompts this skill should handle well
- "Upgrade this Solid 1 app to Solid 2 beta."
- "Can you migrate these components away from createResource, Suspense, and Index?"
- "My tests started failing after moving to Solid 2 next."
- "What is the safest order to migrate this Solid codebase to 2.x?"
Additional resources