[]previous and next page

Reactive primitives

Reactive primitives in the Migrating from Solid and SolidStart guide.

For agentsUpdated
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: