Fix: SolidJS Not Working — Signal Not Updating, Effect Running Twice, or createResource Data Undefined
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 system is fundamentally different from React:
- Signals must be accessed (called) inside a reactive context —
count()is a getter function. Reading its value outside JSX, effects, or memos “escapes” the reactive tracking. Assigningconst value = count()outside JSX reads the value once and never tracks changes. - Effects track signal reads automatically — you don’t declare dependencies. Any signal read inside
createEffect()is tracked. If you call an async function inside an effect, signals read after the firstawaitare not tracked. - Components run only once — unlike React, SolidJS components don’t re-run on state change. Only the reactive parts (JSX expressions, effects, memos) update. This is a feature, not a bug, but it breaks patterns like
const x = signal()at the component level. createResourceis async — access data inside JSX —data()returnsundefinedwhile loading, then the resolved value. Always access it inside JSX or effects, and usedata.loadingto check status.
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);
}
});For related frontend framework issues, see Fix: SvelteKit Not Working and Fix: Vue Composable Not Reactive.
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.