solid-js-2.x-api-changes-and-best-practices — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited solid-js-2.x-api-changes-and-best-practices (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 for Solid 2.x beta or next work. Treat the current beta migration guide and RFCs as the source of truth, and be explicit that some APIs or package boundaries may still shift before final stable release.
Determine whether the user wants:
If the user is actually upgrading old code, prefer the dedicated migration skill unless they only want a narrow API explanation.
Read this skill in passes instead of treating every section as equally important.
Important framing, Primitive selection guide, and Core behavior changes to internalize.Core API guide for fresh code, reviews, and narrow API questions.Reference guide when the task is concentrated in one area such as async data, ownership, stores, or DOM behavior.Task-focused loading and Common mistakes to catch for code review, warning triage, and implementation guidance.next.createAsync, mount, router helpers, or Start-specific patterns unless the user is explicitly in that ecosystem and the local repo confirms those exports.Keep the main body of this skill as the working guide. Read the deeper references when the task is concentrated in one area.
onSettled, or ownership: read references/reactivity-effects-and-ownership.mdLoading, isPending, latest, refresh, action, or optimistic UI: read references/async-data-and-mutations.mdcreateStore, createProjection, storePath, snapshot, merge, or omit: read references/stores-and-helpers.mdFor, Repeat, function-child accessors, DOM attributes, ref directive factories, or context providers: read references/control-flow-dom-and-context.mdChoose primitives with this bias:
createSignal for local scalar statecreateMemo for derivation, including async derivation when it matches the problemcreateStore for nested stateLoading and Errored for async and error boundariesisPending, latest, and refresh for async coordinationaction plus optimistic helpers for mutationscreateProjection or function-form createStore for projection-oriented derived collections and keyed reconciliationcreateEffect only for bridging to the outside worldFor, Show, Switch, Match, and Repeat for control flowref factories for imperative DOM hooksKeep this dense and practical. Bias toward use it for, watch out for, and the most important 2.x-specific behavior.
createSignalconst [value, setValue] = createSignal(() => props.something);For readonly derivation, prefer createMemo.
createMemoawait.createAsync.const profile = createMemo(async () => {
const id = userId();
return fetchProfile(id);
});Loading boundary handles them.Track before await:
const filtered = createMemo(async () => {
const q = query();
await warmCache(q);
return search(q);
});Do not read query() for the first time after an await.
createEffectcreateMemo, not createEffectExample:
createEffect(
() => [title(), enabled()],
([nextTitle, nextEnabled]) => {
document.title = nextTitle;
widget.setEnabled(nextEnabled);
return () => widget.dispose();
}
);onSettledonMount.onMount migration as a semantic review, not a blind rename.LoadingcreateMemo(async () => ...) or another clearly derived 2.x primitive. Do not answer with old Suspense code unless the user asked for comparison or migration context.ErroredisPending, latest, and refreshisPending(() => expr) is for refresh or stale-while-revalidate states, not first load.latest(fn) lets you inspect the most recent in-flight value.refresh(...) recomputes derived reads after writes.Example:
const users = createMemo(async () => fetchUsers(filter()));
const usersPending = () => isPending(() => users());
const latestUsers = () => latest(users);action, createOptimistic, createOptimisticStoreaction(...) for mutation flows.Example:
const [todos] = createStore(() => api.listTodos(), []);
const [optimisticTodos, setOptimisticTodos] = createOptimisticStore(() => todos(), []);
const addTodo = action(function* (text: string) {
const optimistic = { id: crypto.randomUUID(), text, pending: true };
setOptimisticTodos(list => list.push(optimistic));
const saved = yield api.addTodo(text);
refresh(todos);
return saved;
});Keep the real source of truth explicit and refresh it after the mutation settles.
createProjectioncreateStoresolid-js.createStore(fn, initial) still exists for derived-store patterns when that shape is the right fit.snapshot replaces unwrap, and returning a value from a setter performs a top-level shallow replacement or diff.Preferred update shape:
setStore(s => {
s.user.address.city = "Paris";
});storePathCompatibility helper for path-style store updates. Useful during migration, but not the default style for fresh 2.x code.
Example:
setStore(storePath("user", "address", "city", "Paris"));merge and omitGeneral helpers replacing mergeProps and splitProps. merge treats undefined as an explicit value, not as "missing". omit is the preferred way to exclude keys without split-style copying.
Example:
const merged = merge({ a: 1, b: 2 }, { b: undefined });snapshot and deepUse snapshot(store) for serialization or interop. Use deep(store) only when deep observation is truly intended.
Example:
const plain = snapshot(store);For, Show, Switch, Match, RepeatFor replaces Index via keyed={false}.Repeat handles count or range rendering without list diffing.createContextUse the context object itself as the provider component.
Example:
const ThemeContext = createContext("light");
<ThemeContext value="dark">
<Page />
</ThemeContext>Prefer this over carrying forward Context.Provider.
createRootcreateRoot(...) created inside an owned scope is owned by its parent by default.If the user genuinely needs detached lifetime, make that explicit:
const singleton = runWithOwner(null, () => {
const [value, setValue] = createSignal(0);
return { value, setValue };
});createComputed removalcreateComputed is removed.createEffect for side effects, function-form createSignal or createStore for derived state with a setter, or createMemo for readonly derivation.Use ref factories for imperative DOM hooks and directive-like behavior. Do not translate use: literally.
flush() only when a synchronous settled point is truly needed, especially in tests or imperative DOM code.This interacts directly with async derivations and split effects. Do not reason about 2.x as though setters synchronously rewire every downstream read.
Prefer the 2.x shape:
createEffect(
() => source(),
value => {
sideEffect(value);
return () => cleanup();
}
);The compute function tracks dependencies. The apply function performs effects and can return cleanup.
Use this bias:
Be unusually strict about this. If the user asks about createEffect, explain the split even if the immediate bug looks small, because this is one of the main 2.x mental-model changes.
In component bodies, avoid reading reactive values at top level unless the read is intentionally wrapped in untrack. This includes common patterns like destructuring reactive props.
Prefer reading inside JSX expressions, memos, or effect compute functions.
Do not use memos or tracked scopes to write application state back into signals or stores. Prefer:
createMemopureWrite: true only for valid internal casesDo not recommend pureWrite: true as a generic warning silencer.
Solid 2.x can derive async values directly. Show that this is possible, but do not force it as the one true answer for every async problem.
Use this guidance:
createMemo(async () => ...) is one viable 2.x patterncreateProjection(...) or function-form createStore(...) may fit better than a plain memoLoadingisPending and optionally latestaction plus optimistic primitives over hand-rolled pending flagsImportant distinctions:
Loading boundary handles themLoading is for first readinessisPending is for background revalidation when usable UI already existsconst profile = createMemo(async () => {
const currentId = userId();
return fetchProfile(currentId);
});
<Loading fallback={<Spinner />}>
<ProfileView profile={profile()} />
</Loading>The accessor is still read normally. The async part is expressed by suspension and the boundary, not by adding a separate resource API.
const [feed] = createStore(async () => {
const items: Message[] = [];
for await (const chunk of getMessageStream(roomId())) {
items.push(...chunk);
}
return items;
}, [], { key: "id" });
const [projectedFeed] = createStore(
draft => {
const visible = feed().filter(m => !m.hidden);
draft.items = visible;
draft.unread = visible.filter(m => !m.read).length;
},
{ items: [], unread: 0 }
);
const [optimisticFeed, setOptimisticFeed] = createOptimisticStore(() => projectedFeed(), {
items: [],
unread: 0
});
const sendMessage = action(function* (text: string) {
const optimistic = { id: crypto.randomUUID(), text, pending: true };
setOptimisticFeed(s => {
s.items.unshift(optimistic);
s.unread += 1;
});
yield api.sendMessage(roomId(), text);
refresh(feed);
refresh(projectedFeed);
});
const feedRefreshing = () => isPending(() => projectedFeed().items);Use this kind of shape when:
isPending should describe refresh state without replacing the current UIsolid-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 or function-form createStoreunwrap -> snapshotclassList -> classcreateResource removed -> async computations plus LoadingstartTransition and useTransition removed -> built-in transitions, isPending, Loading, and optimistic primitivesbatch removed -> flush() when immediate application is truly neededContext.Provider removed -> use the context directly as provider componentcreateComputed removed -> split createEffect, function-form createSignal or createStore, or createMemouse: directives removed -> ref directive factoriesattr: and bool: removed -> standard attribute behavioroncapture: removedonMount -> onSettledUse the quick map above for package moves. The main practical change is that DOM or renderer subpaths often move to @solidjs/*, while store APIs move into solid-js.
isPending(() => expr) for stale-while-refreshing indicatorslatest(fn) to inspect in-flight values when neededrefresh(...) to recompute reads after mutationsThe rename and removal map above covers the high-level swaps. This section is about how to use the surviving async pieces together.
Index is removed; use <For keyed={false}>For child functions receive accessors, so call item() and i()Show and related APIs may also receive accessorsRepeat exists for count or range-based renderingsetStore(draft => { ... })createSelector patterns generally move to createProjection or function-form createStorestorePath(...) exists as a compatibility helper when path-style ergonomics are still usefulunwrap(store) -> snapshot(store)mergeProps -> mergesplitProps -> omitdeep(store) is available when deep observation is truly neededcreateSignal(fn) and createStore(fn, initial) support derived formsuse: directives are removed; use ref directive factoriesclassList moves to class object or array formsattr: and bool: namespaces are removedoncapture: is removedContext.Provider becomes using the context directly as the provider componentWhen writing or reviewing 2.x code:
flush() sparingly and usually only after you can explain why it is neededLoading for initial readiness and isPending for refresh state instead of inventing extra flagsref factories over trying to recreate use: mentallyContext.Provider boilerplaterender with unrelated mount APIs unless the local repo or installed exports show that is the right moveLoad these ideas first:
Primitive selection guidecreateMemoLoading and Errored for boundariesaction plus optimistic helpers for mutationsLoad these ideas first:
flush()createEffectcreateRootCommon warning-to-cause mapping:
Look for these first:
createMemocreateRoot(...) calls that assume detached lifetime without making that explicititem() or i()Load these ideas first:
createMemo(async () => ...) for readsLoading for first readinessisPending for revalidation statelatest when inspecting current in-flight value mattersaction and optimistic primitives for writesrefresh(...) after mutationsKeep the job split explicit:
refresh(...) recomputes reads after writes; it is not just a renamed refetch habitLoad these ideas first:
createStore setterscreateProjection for store-shaped derived resultscreateStore when derived store state fits better therestorePath only as a compatibility helper, not the fresh-code defaultsnapshot, merge, and omit instead of old 1.x helpersLoad these ideas first:
ref directive factories instead of use:class object or array forms instead of classList| Mistake | Better direction |
|---|---|
| Assuming a setter read updates immediately | Explain microtask batching and use flush() only when a settled point is truly needed. |
Using createEffect for derivation | Move derivation to createMemo or another derived form. |
| Reading reactive props at the top level of a component | Read them inside JSX, memos, or effect compute functions. |
| Writing app state from memos or tracked scopes | Move writes to actions, event handlers, or explicit writable derived-state patterns. |
Using pureWrite: true as a generic warning silencer | Reserve it for narrow internal cases such as refs or internal bookkeeping; fix the state flow instead. |
Using Suspense in fresh 2.x core examples | Use Loading unless the user asked for migration comparison. |
| Treating async reads and async mutations as the same kind of problem | Use computations for reads and action(...) for writes. |
Reaching for createAsync, router helpers, or Start helpers in core Solid prompts | Stay on Solid core primitives unless the task explicitly moves into that ecosystem. |
Treating storePath as the fresh-code default | Prefer draft-first store setters in new 2.x code. |
| Forgetting accessor semantics in control-flow callbacks | In For keyed={false} and similar callback sites, read values with item() and i() when accessors are provided. |
Translating use: directives literally | Use ref directive factories. |
Treating isPending like first-load state | Use Loading for first readiness and isPending for refresh or stale-while-revalidate state. |
Using Context.Provider out of habit | Use the context object directly as the provider component. |
Assuming nested createRoot(...) is detached by default | Explain that nested roots are owned by the parent unless detached explicitly with runWithOwner(null, ...). |
Replacing render with a plausible-sounding mount helper | Only move imports or APIs when the local repo or official 2.x docs confirm it. |
When answering, prefer this structure:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.