solid-js-2.x-migration — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited solid-js-2.x-migration (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
Treat the migration as two kinds of work:
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.
Read this skill in passes instead of treating every section as equally important.
Migration mindset, Recommended migration workflow, and High-confidence mechanical migrations.Canonical before and after rewrites for the first implementation pass.Semantics that need careful migration when behavior, warnings, tests, or lifetime issues are involved.Task-focused migration guidance and Common migration mistakes to catch for review, triage, and planning.Apply these first when present:
solid-js/web -> @solidjs/websolid-js/store imports -> solid-jssolid-js/h -> @solidjs/hsolid-js/html -> @solidjs/htmlsolid-js/universal -> @solidjs/universalSuspense -> LoadingErrorBoundary -> ErroredIndex -> For keyed={false}createSelector -> createProjection or function-form createStoreunwrap -> snapshotmergeProps -> mergesplitProps -> omitclassList -> classuse: directives -> ref directive factoriesattr: and bool: -> standard attribute behavioroncapture: removedonMount -> onSettledEven 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 LoadingstartTransition and useTransition go away; express pending and transition UX with built-in transitions, isPending, Loading, and optimistic primitivescreateComputed is removed; replace it based on intent rather than by pattern-matching syntaxKeep rewrites terse and behavior-aware.
// 1.x
import { render } from "solid-js/web";
import { createStore } from "solid-js/store";
// 2.x beta
import { render } from "@solidjs/web";
import { createStore } from "solid-js";Index to For// 1.x
<Index each={items()}>{(item, i) => <Row item={item()} index={i} />}</Index>
// 2.x beta
<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// 1.x
<Suspense fallback={<Spinner />}>
<Page />
</Suspense>
// 2.x beta
<Loading fallback={<Spinner />}>
<Page />
</Loading>ErrorBoundary to Errored// 1.x
<ErrorBoundary fallback={(err, reset) => <Fallback err={err} reset={reset} />}>
<Page />
</ErrorBoundary>
// 2.x beta
<Errored fallback={(err, reset) => <Fallback err={err} reset={reset} />}>
<Page />
</Errored>onMount to onSettled// 1.x
onMount(() => {
measureLayout();
});
// 2.x beta
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.
// 1.x
createEffect(() => {
document.title = title();
});
// 2.x beta
createEffect(
() => title(),
value => {
document.title = value;
}
);// 1.x
createEffect(() => {
const id = setInterval(() => console.log(name()), 1000);
onCleanup(() => clearInterval(id));
});
// 2.x beta
createEffect(
() => name(),
value => {
const id = setInterval(() => console.log(value), 1000);
return () => clearInterval(id);
}
);// 1.x style that now warns
function Title(props) {
const t = props.title;
return <h1>{t}</h1>;
}
// 2.x beta
function Title(props) {
return <h1>{props.title}</h1>;
}// 1.x
const plain = unwrap(store);
const merged = mergeProps(defaults, overrides);
const [local, rest] = splitProps(props, ["class"]);
// 2.x beta
const plain = snapshot(store);
const merged = merge(defaults, overrides);
const rest = omit(props, "class");Important semantic change:
merge, undefined is an explicit override value, not a skipped keycreateSelector to projection// 1.x
const isSelected = createSelector(selectedId);
// 2.x beta
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.
// 1.x style
setStore("user", "address", "city", "Paris");
// 2.x preferred
setStore(s => {
s.user.address.city = "Paris";
});
// 2.x compatibility bridge
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.
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.
createEffectMigrate from single-callback effects to compute/apply form when the effect depends on reactive reads and performs side effects.
When migrating:
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 onSettledReplace 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// 1.x
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
// 2.x beta
<ThemeContext value="dark">
<Page />
</ThemeContext>createComputed replacementChoose the replacement by intent:
createEffectcreateSignal or createStorecreateMemo// 1.x
createComputed(() => {
setValue(props.input);
});
// 2.x beta, writable derived state
const [value, setValue] = createSignal(() => props.input);If the old createComputed was really doing side effects, migrate to split createEffect instead.
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 };
});2.x warns on top-level reactive reads. Migration often requires:
untrack only when a one-time read is truly intentionalThis 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.
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.
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-yetisPending for updating-againlatest when stale-while-refreshing output should remain visibleReasonable 2.x migration targets include:
createMemo(async () => ...) for async derived valuescreateStore(fn, initial) or createProjection(...) for async derived collections or keyed reconciled resultsFor selector-heavy or keyed-list code, createProjection(...) is often the more faithful migration target.
// 1.x
const [user] = createResource(id, fetchUser);
// 2.x beta
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);
});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.
Solid 2.x beta or next migrations may require temporary compatibility work, especially around:
nextIf a dependency has not caught up, say so explicitly and separate framework migration work from ecosystem breakage.
Useful migration-era insights from real projects:
solid-js/web aliased to @solidjs/webnext tags and temporary overrides may be the practical path during beta adoptiondeep(store) read.loading and transition-wrapper patterns should collapse into Loading, isPending, actions, and optimistic primitivesrunWithOwner(null, ...)Load these ideas first:
storePath(...) only when they reduce migration riskLook for these first:
Index to For keyed={false} with accessor-aware callbacksonMount assumptions that need onSettledContext.Provider habitsuse: directives that need ref factoriesLook for these first:
createMemocreateRoot(...) calls that assumed detached lifetimeLoad these ideas first:
Suspense -> LoadingLoadingLoading and isPending as different jobsaction(...), optimistic primitives, and refresh(...)Look for these first:
| 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. |
Ask for confirmation when:
Otherwise, keep moving through the migration.
When migrating code, prefer this structure:
When possible, include one-line reasoning beside each migration hotspot so the user understands why the rewrite happened.
For code rewrites, keep these guardrails:
createAsync or mount unless the local repo proves they are the right target~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.