Reactive primitives
Reactive primitives in the Migrating from Solid and SolidStart guide.
| Solid | ShadowJS | What differs |
|---|---|---|
createSignal(v, { equals, name }) |
useSignal(v, { equals, name }) |
Returns [get, set]. The setter accepts a value or (prev) => next. Default equality is Object.is, so writing NaN over NaN does not notify (Solid uses ===). The initial value argument is required: useStore<T | undefined>(undefined) replaces createSignal<T>(). |
createMemo(fn, init, { equals }) |
useMemo(fn, { equals, lazy, name }) |
There is no initial-value argument. fn(prev) receives undefined on its first run. lazy: true defers the first run to the first read. |
createEffect(fn) |
useEffect(fn) |
Runs after render, never on the server. The first run is deferred to the flush. The returned function is the cleanup; the return value is not fed back as prev. |
createEffect(on(dep, fn, { defer })) |
useEffect(() => dep(), (value, prev) => fn(value, prev), { defer: true }) |
There is no on(). The first function is tracked, the second is not. |
createRenderEffect(fn) |
useRenderEffect(fn) or useRenderEffect(compute, apply) |
Runs once synchronously when created, then at each flush. effect from @shadow-js/web is the same function under the compiler's name. |
createComputed(fn) |
none | Use useMemo for derivations. Writing a signal inside a memo throws WRITE_IN_MEMO in development. |
batch(fn) |
batch(fn) |
Same. Effects run once at the end of the outermost batch. |
untrack(fn) |
untrack(fn) |
Same. There is no .peek(); untrack(count) reads without subscribing. |
createRoot(fn) |
createRoot(fn, { detached }) |
A root created inside an owner is attached to it and disposed with it. Solid roots are never owned by the surrounding scope; pass { detached: true } for that behavior. dispose is always provided. |
getOwner(), runWithOwner(o, fn) |
same names | runWithOwner rethrows what fn throws; Solid routes it to the owner's error handler and returns undefined. |
onMount(fn) |
onMount(fn) |
Same timing. A function returned from fn is used as a cleanup (Solid ignores it). |
onCleanup(fn) |
onCleanup(fn) |
Returns void, not fn. |
onError(fn), catchError(fn, handler) |
same names | Same. |
createSelector(source, fn?) |
useSelector(source, equals?) |
Returns isSelected(key). equals(key, value); there is no third options argument. |
createUniqueId() |
useId() |
Throws outside an owner. The id comes from the owner path, and the client creates more owners than the server, so it can differ between a server render and its hydration. useBaseId from @shadow-js/primitives derives ids from the hydration position instead. |
createContext(default), useContext(ctx) |
same names | useContext throws (CONTEXT_MISSING) when there is no provider and no default; Solid returns undefined. Both <Ctx value={v}> and <Ctx.Provider value={v}> work. |
children(fn) |
children(fn) |
Same, plus toArray(). |
mergeProps, splitProps |
same names | A key listed in several splitProps groups goes to every group that lists it (Solid: the first group only). |
mapArray, indexArray |
same names | mapArray(list, map, { key }) is a ShadowJS extension: the callback then receives an item accessor. |
createResource |
useResource |
See Resources. |
createReaction |
useReaction(onInvalidate) |
See Not available. |
from, observable, enableExternalSource |
useExternalStore(subscribe, getSnapshot) |
See Not available. |
createDeferred |
useDeferredValue |
Renamed. startTransition and useTransition have the same names. See Transitions. |
| none | useOptimistic(source, reducer) |
Optimistic state until a promise settles. |
Counter: signals, memo and effect#
Solid (not executed):
tsx
import { createEffect, createMemo, createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const double = createMemo(() => count() * 2);
createEffect(() => {
document.title = `count ${count()}`;
});
return (
<button onClick={() => setCount((n) => n + 1)}>
{count()} / {double()}
</button>
);
}
ShadowJS: three renames, nothing else.
src/counter.tsxtsx
import { useEffect, useMemo, useStore } from "@shadow-js/core";
export function Counter() {
const [count, setCount] = useStore(0);
const double = useMemo(() => count() * 2);
useEffect(() => {
document.title = `count ${count()}`;
});
return (
<button type="button" onClick={() => setCount((n) => n + 1)}>
{count()} / {double()}
</button>
);
}
Timing: effects flush in a microtask#
Solid re-runs an effect after every write outside a batch. ShadowJS queues effects and flushes them in a microtask
(ADR-007), so two writes in the same tick give one run with the final state. A memo is different: it is fresh as
soon as the write returns. Use flushSync() (or flush() from @shadow-js/testing) when a test or an imperative
read must see effect results.
Render effects, error routing and the list helpers keep their Solid names and shapes: