Fix: SolidJS Not Working — Signal Not Updating, Effect Running Twice, or createResource Data Undefined
Part of: React & Frontend Errors
Quick Answer
How to fix SolidJS reactivity issues — signal access inside JSX, effect dependencies, createResource with loading states, Show and For components, store mutations, and common mistakes coming from React.
The Problem
A signal update doesn’t trigger a re-render:
const [count, setCount] = createSignal(0);
function Counter() {
const value = count(); // Destructured — loses reactivity
return <div>{value}</div>; // Never updates
}Or an effect fires immediately and then never again:
createEffect(() => {
const data = fetchData(); // Not a signal — effect won't re-run
console.log(data);
});Or createResource returns undefined even after data loads:
const [data] = createResource(fetchUsers);
console.log(data()); // undefined — called synchronously before fetch resolvesOr a For list doesn’t update when the array changes:
const [items, setItems] = createSignal([1, 2, 3]);
setItems([...items(), 4]);
// List doesn't show 4 — For component didn't updateWhy This Happens
SolidJS’s reactivity is fundamentally different from React’s, and almost every “signal not updating” symptom traces back to one of three misconceptions inherited from React.
The first misconception is that components re-run. In React, every state change re-executes the component function and rebuilds JSX. In Solid, the component function runs exactly once at mount. The JSX is compiled into fine-grained DOM operations, and only the specific expressions that read signals re-evaluate. This means const x = count() at the top of a component reads the value once at mount and never again. Even though it looks correct, it’s frozen. The fix is to keep the call inside JSX: {count()} or inside a createMemo.
The second misconception is that destructuring is free. In React, function Component({ name }) is idiomatic. In Solid, destructuring props breaks reactivity because the destructured locals are evaluated once. props.name is a getter — reading props.name inside JSX tracks the parent’s signal. Reading const { name } = props evaluates the getter once and loses the connection. The Solid linter warns about this, but it’s the single most common React-to-Solid bug.
The third misconception is that async automatically tracks. Effects track every signal read during synchronous execution. Once you hit an await, you’re in a microtask and the tracking scope has closed. Signals read after the await are not tracked, and the effect won’t re-run when they change. The fix is to read signals before any await, or use createResource which manages async tracking for you.
There’s a fourth category of issue around control flow. Show when={...} and For each={...} are reactive primitives, but using a ternary or array.map() inside JSX defeats the fine-grained updates. A ternary tears down the whole branch on every change; For rearranges DOM nodes by identity. Mix them up and you get unnecessary remounts or stale state.
Diagnostic Timeline
Minute 0 — Signal change doesn’t update the DOM. Your first instinct is to wrap it in another createSignal or call setCount differently. It almost never helps. Open the file and look at where the signal is read. If you see const value = count() outside JSX, that’s the bug. Solid runs the component body once.
Minute 3 — Check for destructured props. If you wrote function Counter({ value }), the value local is frozen at mount. Switch to function Counter(props) and use props.value everywhere. Yes, it looks less ergonomic; yes, it’s mandatory for reactivity. Solid ships an ESLint rule that catches this — enable solid/reactivity if you haven’t.
Minute 7 — Verify the read happens inside JSX. Open the component and confirm every signal read is inside a JSX expression {signal()}, inside createEffect, or inside createMemo. Reads in the function body, in useEffect-style cleanup, or in event handlers outside a closure don’t track. Event handlers are an exception — they intentionally don’t track because they fire once per event.
Minute 12 — Effect runs once and stops. This means the signal you expected to track wasn’t read during the first synchronous execution. Common cause: the read is inside an if branch that was false on first run. Convert to createEffect(on(signal, ...)) to declare dependencies explicitly, or restructure the code so the read happens unconditionally on every run.
Minute 18 — createResource returns undefined forever. Either the source signal is undefined/null (which prevents the fetcher from running), or the fetcher throws and you’re not handling resource.error. Wrap the JSX in <Suspense fallback={...}> and let Solid manage the loading state, or check resource.loading and resource.error before reading resource().
Minute 25 — For list won’t update after setItems. For keys by identity (reference equality). If you wrote setItems([...items()]) to “trigger” a re-render React-style, the elements are the same references — For sees no change. The fix is to add or remove items by content, not by re-spreading. For primitive arrays, use <Index> instead of <For> — Index keys by position.
Minute 32 — Show vs ternary mismatch. A ternary like {condition() && <Component />} tears down Component whenever condition() flips. <Show when={condition()}> does the same. But <Show when={condition()} keyed>{(value) => ...} reactively rebuilds when the value itself changes. Mixing the keyed and non-keyed forms produces “child component remounts unexpectedly” symptoms.
Minute 40 — Signal read outside a tracked scope warning. Solid prints computations created outside a createRoot or render will never be disposed in the console. This usually means you called createSignal or createEffect at the module top level instead of inside a component or createRoot. Move the call into a component, or wrap it explicitly: createRoot(dispose => { ... }).
Fix 1: Understand Signal Reactivity
import { createSignal, createEffect, createMemo } from 'solid-js';
// Basic signal
const [count, setCount] = createSignal(0);
// WRONG — reading signal outside reactive context loses tracking
function Counter() {
const value = count(); // Read once — never updates
return <div>{value}</div>;
}
// CORRECT — access signal directly inside JSX
function Counter() {
return <div>{count()}</div>; // Reactive — updates when count changes
}
// CORRECT — use in effect (also reactive)
createEffect(() => {
console.log('Count changed:', count()); // Re-runs when count changes
});
// CORRECT — use in memo for derived values
const doubled = createMemo(() => count() * 2);
function Display() {
return <div>{doubled()}</div>; // doubled() is also reactive
}
// Updating signals
setCount(5); // Set value directly
setCount(prev => prev + 1); // Update based on previous value
// Signal with objects — replace, don't mutate
const [user, setUser] = createSignal({ name: 'Alice', age: 30 });
setUser({ ...user(), age: 31 }); // Spread to create new object
// setUser().age = 31; // WRONG — mutation doesn't trigger updateSignals in event handlers:
function LoginForm() {
const [email, setEmail] = createSignal('');
const [password, setPassword] = createSignal('');
function handleSubmit(e: Event) {
e.preventDefault();
// Read signals in event handler — fine (not tracked, just reading)
loginUser(email(), password());
}
return (
<form onSubmit={handleSubmit}>
<input
value={email()}
onInput={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password()}
onInput={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}Fix 2: Fix Effects and Reactive Dependencies
SolidJS effects automatically track any signal accessed during synchronous execution:
import { createEffect, createSignal, on } from 'solid-js';
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
// Tracks both a and b — re-runs when either changes
createEffect(() => {
console.log('Sum:', a() + b());
});
// PROBLEM — async breaks tracking
createEffect(async () => {
const result = await someAsyncFn();
console.log(a()); // NOT tracked — after await, tracking is lost
});
// CORRECT — read signals before the await
createEffect(async () => {
const value = a(); // Tracked — read synchronously
const result = await someAsyncFn(value);
console.log(result);
});
// Explicit dependency with on() — only re-run when a changes, ignore b
createEffect(on(a, (currentA, prevA) => {
console.log('A changed from', prevA, 'to', currentA);
// b() here is NOT tracked — on() narrows dependencies
}));
// Deferred effect — skip the first run
createEffect(on(a, (value) => {
console.log('A changed (not on mount):', value);
}, { defer: true }));
// Cleanup in effects
createEffect(() => {
const interval = setInterval(() => {
console.log('tick', count());
}, 1000);
onCleanup(() => clearInterval(interval)); // Runs before next effect or on unmount
});Fix 3: Use createResource for Async Data
import { createResource, createSignal, Suspense } from 'solid-js';
// Basic resource — no parameters
const [users, { refetch, mutate }] = createResource(async () => {
const res = await fetch('/api/users');
return res.json() as Promise<User[]>;
});
// Resource with reactive source — refetches when source changes
const [userId, setUserId] = createSignal<number | null>(null);
const [user, { refetch }] = createResource(
userId, // Source signal — resource refetches when this changes
async (id) => {
if (!id) return null;
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<User>;
}
);
// Access resource state
function UserProfile() {
return (
<div>
{/* Check loading state */}
{user.loading && <Spinner />}
{user.error && <p>Error: {user.error.message}</p>}
{/* Access data — undefined while loading */}
{user() && <h1>{user()!.name}</h1>}
</div>
);
}
// Better — use Suspense and ErrorBoundary
function UserProfile() {
return (
<Suspense fallback={<Spinner />}>
<ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}>
<UserContent />
</ErrorBoundary>
</Suspense>
);
}
function UserContent() {
// With Suspense, user() is always defined here (Suspense handles loading)
return <h1>{user()!.name}</h1>;
}
// Optimistic updates with mutate
function updateUserName(newName: string) {
mutate(prev => prev ? { ...prev, name: newName } : prev); // Optimistic
updateUserApi(newName).catch(() => refetch()); // Revert on error
}Fix 4: Use Show and For Correctly
SolidJS provides reactive control flow components:
import { Show, For, Index, Switch, Match, ErrorBoundary } from 'solid-js';
// Show — conditional rendering
function UserStatus() {
const [user, setUser] = createSignal<User | null>(null);
return (
<Show
when={user()}
fallback={<p>Not logged in</p>}
keyed // When true, re-renders when user() changes (not just truthy/falsy)
>
{(u) => <p>Welcome, {u.name}</p>}
</Show>
);
}
// For — list rendering (keyed by identity)
function UserList() {
const [users, setUsers] = createSignal<User[]>([]);
return (
<ul>
<For each={users()} fallback={<li>No users</li>}>
{(user, index) => (
<li>{index() + 1}. {user.name}</li>
)}
</For>
</ul>
);
}
// Index — list rendering where order matters more than identity
// Re-renders items when their content changes, not when they move
function NumberList() {
const [numbers, setNumbers] = createSignal([1, 2, 3]);
return (
<Index each={numbers()}>
{(number, i) => <span>{number()}</span>}
{/* number is a signal here — updates in place */}
</Index>
);
}
// Switch/Match — multiple conditions
function StatusBadge({ status }: { status: () => string }) {
return (
<Switch fallback={<span>Unknown</span>}>
<Match when={status() === 'active'}><span class="green">Active</span></Match>
<Match when={status() === 'inactive'}><span class="gray">Inactive</span></Match>
<Match when={status() === 'pending'}><span class="yellow">Pending</span></Match>
</Switch>
);
}Why For vs Index:
For— tracks items by reference. Moving an item in the array moves the DOM node. Use for objects with stable identity.Index— tracks items by position. Re-renders at a position when the item changes. Use for primitive arrays or when position matters.
Fix 5: Use createStore for Complex State
For nested state that would require many signals, use createStore:
import { createStore, produce, reconcile } from 'solid-js/store';
interface AppState {
users: User[];
selectedId: number | null;
settings: {
theme: 'light' | 'dark';
language: string;
};
}
const [state, setState] = createStore<AppState>({
users: [],
selectedId: null,
settings: { theme: 'light', language: 'en' },
});
// Update nested paths — fine-grained reactivity
setState('settings', 'theme', 'dark');
setState('selectedId', 5);
// Add to array
setState('users', users => [...users, newUser]);
// Update specific array item by index
setState('users', 0, 'name', 'Bob');
// Update by condition
setState('users', user => user.id === targetId, 'active', true);
// produce — Immer-like mutable updates
setState(produce((draft) => {
const user = draft.users.find(u => u.id === targetId);
if (user) {
user.name = 'Bob';
user.role = 'admin';
}
}));
// reconcile — diff-and-patch large objects (from API responses)
const freshData = await fetchUsers();
setState('users', reconcile(freshData)); // Only updates changed parts
// Access in components — fine-grained: only re-renders affected parts
function UserList() {
return (
<For each={state.users}>
{(user) => (
// Only re-renders when THIS user's name changes
<li class={state.selectedId === user.id ? 'selected' : ''}>
{user.name}
</li>
)}
</For>
);
}Fix 6: Context and Dependency Injection
import { createContext, useContext, ParentComponent } from 'solid-js';
// Define context
interface CounterContextType {
count: () => number;
increment: () => void;
decrement: () => void;
}
const CounterContext = createContext<CounterContextType>();
// Provider component
export const CounterProvider: ParentComponent = (props) => {
const [count, setCount] = createSignal(0);
const context: CounterContextType = {
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
};
return (
<CounterContext.Provider value={context}>
{props.children}
</CounterContext.Provider>
);
};
// Consume context
function CounterDisplay() {
const ctx = useContext(CounterContext);
if (!ctx) throw new Error('CounterDisplay must be inside CounterProvider');
return <span>{ctx.count()}</span>;
}
// Usage
function App() {
return (
<CounterProvider>
<CounterDisplay />
</CounterProvider>
);
}Still Not Working?
Component renders correctly on first load but never updates — you’re reading a signal outside JSX. In SolidJS, component functions run exactly once. Any signal read in the function body (but outside JSX or a createEffect) reads the initial value and is never re-tracked. Move signal accesses into the JSX return or wrap them in createMemo:
// WRONG
function MyComponent() {
const text = label(); // Reads once at component creation
return <p>{text}</p>;
}
// CORRECT
function MyComponent() {
return <p>{label()}</p>; // Read inside JSX — reactive
}createEffect runs once and stops — if the signals you expect to track aren’t read synchronously during the first run, they aren’t tracked. Check that your signal access isn’t behind an if branch that was false on the first run — those signals aren’t tracked until the branch becomes true.
Infinite effect loop — if an effect writes to a signal it also reads, it creates a cycle. Use untrack() to read a signal without tracking it:
import { untrack } from 'solid-js';
createEffect(() => {
const newValue = sourceSignal();
// Read target without creating a dependency
const current = untrack(() => targetSignal());
if (newValue !== current) {
setTargetSignal(newValue);
}
});Memo recomputes too often — createMemo(() => expensive(a(), b())) runs whenever a or b changes. If you only want it to recompute when a changes, narrow the dependency with on(): createMemo(on(a, () => expensive(a(), untrack(() => b())))). The on() form is explicit about which signals it watches.
Signal updates correctly in dev but not after production build — Solid relies on a Babel plugin (babel-plugin-jsx-dom-expressions) or a Vite plugin to compile JSX into reactive primitives. If the plugin isn’t running (wrong preset order in babel.config.json, missing vite-plugin-solid), the JSX becomes plain React-style createElement calls and reactivity is silently lost. Confirm vite-plugin-solid is in vite.config.ts’s plugins array.
onCleanup doesn’t fire when expected — onCleanup runs when the surrounding reactive scope is disposed. Inside an effect, that means before the next effect run. At the component root, it means when the component is unmounted. If you put onCleanup inside a setTimeout callback or after an await, the surrounding scope has already closed and the cleanup never registers.
For related frontend framework and reactivity issues, see Fix: SvelteKit Not Working, Fix: Vue Composable Not Reactive, Fix: Qwik Not Working, and Fix: Svelte 5 Runes Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: ky Not Working — Requests Failing, Hooks Not Firing, or Retry Not Working
How to fix ky HTTP client issues — instance creation, hooks (beforeRequest, afterResponse), retry configuration, timeout handling, JSON parsing, error handling, and migration from fetch or axios.