Skip to content

Latest commit

 

History

History
230 lines (180 loc) · 9.55 KB

File metadata and controls

230 lines (180 loc) · 9.55 KB
name solid-errors-reactivity-debugging
description Use when SolidJS reactivity is broken, effects are not firing, or store updates are not reflected in the UI. Prevents lost tracking from destructuring, conditional signal access, async tracking loss, and stale closures. Covers diagnostic flowcharts, solid-devtools debugging, effect tracking issues, store propagation failures, and systematic symptom-to-fix resolution. Keywords: reactivity debugging, effect not firing, store not updating, signal tracking, solid-devtools, stale closure, createEffect, UI not updating, why doesn't my UI update, signal not working, reactive value stale, nothing happens when I change state.
license MIT
compatibility Designed for Claude Code. Requires SolidJS 1.x/2.x with TypeScript.
metadata
author version
OpenAEC-Foundation
1.0

solid-errors-reactivity-debugging

Quick Reference

Diagnostic Table: Symptom to Cause to Fix

Symptom Cause Fix
UI never updates after signal change Signal getter not called in JSX: {count} instead of {count()} ALWAYS call the getter: {count()}
Effect runs once, never again Signal accessed outside tracking scope (component body, event handler) Move signal access inside createEffect or JSX expression
Effect ignores some signals Conditional access / early return before signal read Read ALL signals before any conditional logic
Store property not updating in UI Destructured store property: const { name } = store ALWAYS access via store.name in JSX
Store update has no effect Direct mutation: store.name = "x" ALWAYS use setStore("name", "x")
Props stop updating Destructured props: function Comp({ name }) ALWAYS use props.name, use splitProps if needed
Value is stale in timeout/callback Signal value captured: const v = count() then used later Call count() at the point where you need the value
Effect works initially but stops await inside effect breaks synchronous tracking Move async code to a separate function; read signals before await
Memo returns stale value Memo dependency was untracked or destructured Verify all signal getters are called inside memo callback
Store array update not reflected Used store.items.push() instead of setter Use setStore("items", items.length, newItem) or produce

Critical Warnings

NEVER destructure props in the function signature — this reads values once and permanently breaks tracking. ALWAYS accept props as a single object and access properties reactively.

NEVER store a signal getter result in a variable and expect it to stay current — const v = count() captures a snapshot. ALWAYS call the getter where you need the live value.

NEVER place await before signal reads in an effect — await breaks the synchronous tracking scope. ALWAYS read all tracked signals before any await.

NEVER mutate a store directly — store.x = 5 bypasses the reactive system entirely. ALWAYS use setStore or produce.

NEVER use Array.push/splice/pop on store arrays — these mutate without notifying the reactive graph. ALWAYS use setStore with path syntax or produce.


Diagnostic Flowchart: "My UI Is Not Updating"

START: UI is not updating after state change
  |
  +--> Is the value a signal?
  |     |
  |     +--> Are you calling the getter? count() not count
  |     |     |
  |     |     +--> NO --> FIX: Add () to signal reads in JSX
  |     |     +--> YES --> Is the signal read inside a tracking scope?
  |     |                   |
  |     |                   +--> NO (component body, event handler, onMount)
  |     |                   |     --> FIX: Move read into createEffect or JSX expression
  |     |                   |
  |     |                   +--> YES --> Is there an early return or condition BEFORE the read?
  |     |                         |
  |     |                         +--> YES --> FIX: Read all signals before conditionals
  |     |                         +--> NO --> Is there an await before the read?
  |     |                               |
  |     |                               +--> YES --> FIX: Read signals before await
  |     |                               +--> NO --> Check equality: signal uses === by default
  |     |                                     --> FIX: Use { equals: false } for objects
  |
  +--> Is the value from a store?
  |     |
  |     +--> Did you destructure the store? const { x } = store
  |     |     --> FIX: Access as store.x in JSX
  |     |
  |     +--> Did you mutate directly? store.x = 5
  |     |     --> FIX: Use setStore("x", 5)
  |     |
  |     +--> Did you use Array.push/splice?
  |     |     --> FIX: Use setStore or produce
  |     |
  |     +--> Did you use setStore correctly?
  |           --> Verify path syntax: setStore("path", "to", "prop", value)
  |
  +--> Is the value from props?
        |
        +--> Did you destructure props? function Comp({ name })
        |     --> FIX: Use function Comp(props) and access props.name
        |
        +--> Did you spread props? <Child {...props} />
              --> FIX: Use splitProps/mergeProps for reactive forwarding

Tracking Scope Rules

What IS a Tracking Scope

Scope Tracks Dependencies Re-runs
createEffect(() => ...) YES When dependencies change
createMemo(() => ...) YES When dependencies change
createRenderEffect(() => ...) YES When dependencies change (sync)
createComputed(() => ...) YES When dependencies change (before render)
JSX expressions {count()} YES When dependencies change

What is NOT a Tracking Scope

Context Tracks Why
Component function body NO Runs exactly once during setup
Event handlers onClick={() => ...} NO Run on demand, not reactively
onMount(() => ...) NO Explicitly non-tracking, runs once
setTimeout/setInterval callbacks NO Scheduled by the browser, not SolidJS
untrack(() => ...) NO Deliberately suppresses tracking
Code after await NO Async breaks synchronous tracking context

Async Tracking Loss

The SolidJS reactive system tracks dependencies synchronously. Any signal read after an await is NOT tracked because the execution resumes in a new microtask outside the original tracking scope.

// WRONG — signals after await are NOT tracked
createEffect(async () => {
  const response = await fetch("/api/data");
  console.log(count()); // NOT tracked — effect will NOT re-run when count changes
});

// CORRECT — read all signals BEFORE await
createEffect(() => {
  const currentCount = count(); // Tracked
  const url = apiUrl();         // Tracked

  // Non-tracked async work in a separate scope
  (async () => {
    const response = await fetch(url);
    const data = await response.json();
    setResult(data);
  })();
});

SolidJS 2.x Debugging Differences

Aspect 1.x Behavior 2.x Behavior
Update timing Synchronous propagation Microtask-batched — reads stale until flush
Debugging reads Values update immediately after set Values may appear stale until batch flushes
Immediate propagation Default behavior Use flush() when needed
Dev warnings Manual detection only Built-in warnings for accidental top-level reads
Effect model Single createEffect Split compute/apply — inspect each phase separately

When debugging 2.x: if a value appears stale immediately after setSignal(), this is expected microtask batching behavior. The value updates after the current synchronous execution completes. Use flush() only when you explicitly need immediate propagation.


Debugging Strategies

Strategy 1: Isolation Effect

Create a minimal effect that ONLY reads the suspect signal to verify it fires:

// Step 1: Verify the signal itself updates
createEffect(() => {
  console.log("[DEBUG] count =", count());
});

// Step 2: If this fires but your UI does not, the problem is in HOW the signal
// is accessed in your component (destructuring, conditional, async)

Strategy 2: Tracking Scope Verification

Wrap suspect code in createEffect to confirm tracking works:

// If this logs on changes, tracking works — the bug is elsewhere
createEffect(() => {
  console.log("[DEBUG] store.user.name =", store.user.name);
});

Strategy 3: solid-devtools

Install solid-devtools for visual dependency graph inspection:

npm install --save-dev solid-devtools
// vite.config.ts
import devtools from "solid-devtools/vite";

export default defineConfig({
  plugins: [devtools(), solidPlugin()],
});
// Entry point (index.tsx / App.tsx)
import "solid-devtools";

The devtools browser extension shows: signal values, dependency graphs, component tree, and which computations re-run on each update.


Reference Links

Official Sources