SolidJS createSignal reactivity lost (build ok, test fails) in CI
A signal read outside a tracking scope (an effect, memo, or JSX) is captured once and never re-runs. The bundle compiles fine, but a test that mutates the signal then asserts on a plain variable sees the old value and fails in CI.
What this error means
vite build succeeds, but a Vitest run fails because a value derived from createSignal did not update after setX(...), so the assertion reads the initial value.
FAIL src/counter.test.ts > increments count
AssertionError: expected 1 to be 2
const [count, setCount] = createSignal(1);
const doubled = count() * 2; // read once, never reactive
setCount(2);
expect(doubled).toBe(4);Common causes
A signal read outside a tracking scope
Calling the getter in plain code captures the value at that instant; later set calls do not re-evaluate it.
A derived value that is not a memo
Computing count() * 2 into a constant instead of a createMemo freezes it, so the test observes stale data.
How to fix it
Read signals inside a reactive primitive
- Wrap derived values in
createMemoso they recompute on change. - Assert after the reactive graph flushes, reading the getter again.
- For effects in tests, run them inside
createRootso disposal is controlled.
import { createSignal, createMemo } from 'solid-js';
const [count, setCount] = createSignal(1);
const doubled = createMemo(() => count() * 2);
setCount(2);
expect(doubled()).toBe(4);Test effects inside createRoot
Solid effects need an owner; run them under createRoot so they track and dispose deterministically in CI.
import { createRoot } from 'solid-js';
createRoot(dispose => { /* create effects, assert, then */ dispose(); });How to prevent it
- Derive values with
createMemo, not plain expressions. - Read signal getters inside effects, memos, or JSX only.
- Wrap reactive test setup in
createRoot.