.claude/skills/solidjs/solid-core/solid-core-reactivity-model/SKILL.md
Use when reasoning about SolidJS reactivity, debugging tracking issues, or understanding why components run once. Prevents React mental model contamination such as expecting re-renders, virtual DOM diffing, or stale closure patterns. Covers reactive dependency graph, tracking contexts, ownership tree, synchronous execution model, and direct DOM updates. Keywords: SolidJS reactivity, fine-grained, signals, tracking scope, ownership, createEffect, createMemo, no virtual DOM.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer solid-core-reactivity-modelInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
SolidJS has NO virtual DOM. Components run exactly once to set up a reactive dependency graph. Only reactive expressions (effects, memos, JSX bindings) re-execute when their dependencies change. The DOM is updated directly through reactive bindings — no diffing, no reconciliation, no re-rendering.
| Context | Tracking? | Re-executes on change? | Purpose |
|---------|-----------|----------------------|---------|
| createEffect(() => ...) | YES | YES | Side effects |
| createMemo(() => ...) | YES | YES | Cached derived values |
| createRenderEffect(() => ...) | YES | YES | Synchronous render-phase effects |
| createComputed(() => ...) | YES | YES | Pre-render state sync |
| JSX expression {count()} | YES | YES | DOM text/attribute bindings |
| Component function body | NO | NO — runs once | Setup only |
| Event handlers onClick | NO | NO — runs on demand | User interaction |
| onMount(() => ...) | NO | NO — runs once after mount | DOM initialization |
| untrack(() => ...) | NO | NO — explicitly suppressed | Read without subscribing |
Signal (source) Memo (derived) Effect (sink)
[count] ──────────► [double] ──────────► [DOM update]
│ ▲
└──────────────────────────────────────────────┘
(direct subscription)
| Aspect | SolidJS 1.x | SolidJS 2.x |
|--------|-------------|-------------|
| Signal propagation | Synchronous — updates propagate immediately | Microtask-batched — reads reflect after flush |
| Forcing immediate propagation | N/A (already synchronous) | flush() |
| Effect timing | After render, before paint | Compute-then-apply split |
| Lifecycle hook | onMount | onSettled (can return cleanup) |
| Async support | Manual via createResource | First-class — computations can return Promises |
NEVER assume components re-render — SolidJS component functions run ONCE. Only reactive expressions inside tracking scopes re-execute. Placing logic in the component body expecting it to re-run is the #1 cause of broken SolidJS code.
NEVER destructure props or signal results outside a tracking scope — this captures a static snapshot and permanently breaks reactivity.
NEVER use dependency arrays — SolidJS tracks dependencies automatically. There is no equivalent to React's useEffect([deps]) or useMemo([deps]).
NEVER return cleanup functions from effects — use onCleanup() as a separate call instead. Returning a function from createEffect does NOT register cleanup.
NEVER access signals conditionally or after early returns inside effects — signals accessed after an early return or inside an if branch are only tracked when that code path executes, causing intermittent tracking failures.
ALWAYS call signal getters as functions (count(), not count) — the getter function call is what registers the dependency in the tracking scope.
ALWAYS access props directly (props.name) or use splitProps/mergeProps — NEVER destructure props at the function parameter level.
SolidJS compiles JSX into direct DOM creation calls with reactive bindings. There is no virtual DOM tree, no diff algorithm, no reconciliation.
// What you write:
const el = <div class={className()}>{count()}</div>;
// What the compiler produces (conceptual):
const el = document.createElement("div");
createRenderEffect(() => el.className = className());
createRenderEffect(() => el.textContent = count());
Each {expression} in JSX becomes a fine-grained reactive binding. When count() changes, ONLY that specific text node updates — the div element, its attributes, and all sibling nodes remain untouched.
SolidJS organizes reactive computations into an ownership tree. Every computation (effect, memo) is owned by the scope that created it. When a scope disposes, all child computations are automatically cleaned up.
| Primitive | Purpose |
|-----------|---------|
| createRoot(fn) | Creates a new ownership root — computations inside are NOT auto-disposed |
| getOwner() | Returns the current tracking owner (or null outside any scope) |
| runWithOwner(owner, fn) | Executes fn under a specific owner's scope |
| onCleanup(fn) | Registers cleanup for when the current scope disposes or re-executes |
createRoot (application root)
└── Component A (runs once)
├── createEffect (owned by A)
│ └── onCleanup (runs on re-execute or dispose)
├── createMemo (owned by A)
└── Child Component B (owned by A)
└── createEffect (owned by B)
When Component A unmounts, ALL its owned computations (effects, memos, child components) are disposed automatically.
ALWAYS use createRoot when creating reactive computations outside of a component tree — for example, in a standalone reactive system, a test harness, or a module-level subscription.
// WRONG: Effect created outside ownership — memory leak, no cleanup
createEffect(() => console.log(globalSignal()));
// CORRECT: createRoot provides ownership and disposal
const dispose = createRoot((dispose) => {
createEffect(() => console.log(globalSignal()));
return dispose;
});
// Later: dispose() to clean up
Async callbacks lose their tracking owner. Use runWithOwner to restore it:
function AsyncComponent() {
const owner = getOwner();
onMount(async () => {
const data = await fetchData();
// WRONG: createEffect here has no owner (async broke the scope)
// CORRECT: restore the owner explicitly
runWithOwner(owner!, () => {
createEffect(() => console.log(data, someSignal()));
});
});
}
Signal updates propagate immediately. Each setSignal() call triggers all dependent computations synchronously before the next line of code executes.
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
createEffect(() => console.log(a() + b()));
setA(10); // Effect runs immediately: logs 12
setB(20); // Effect runs immediately: logs 30
Use batch() to defer propagation:
batch(() => {
setA(10);
setB(20);
}); // Effect runs ONCE: logs 30
All signal updates are batched automatically. Reads do NOT reflect updates until the microtask batch flushes. Use flush() when immediate propagation is needed.
// 2.x behavior:
setA(10);
setB(20);
// Effects have NOT run yet — batched until microtask flush
flush(); // Force immediate propagation
batch() needed for most casesflush(): Forces immediate propagation when synchronous behavior is requiredonMount renamed to onSettled: Can return a cleanup functioncreateSignal(fn): Derived-but-writable signals (function argument creates derived initial value)untrack() or read in component bodycreateMemocreateEffectcreateRenderEffectcount() not countuntrack()? → This explicitly suppresses trackingdevelopment
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.