Skip to content

Fix: SolidJS Not Working — Signal Not Updating, Effect Running Twice, or createResource Data Undefined

FixDevs ·

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 resolves

Or 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 update

Why This Happens

SolidJS’s reactivity system is fundamentally different from React:

  • Signals must be accessed (called) inside a reactive contextcount() is a getter function. Reading its value outside JSX, effects, or memos “escapes” the reactive tracking. Assigning const 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 first await are 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.
  • createResource is async — access data inside JSXdata() returns undefined while loading, then the resolved value. Always access it inside JSX or effects, and use data.loading to 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 update

Signals 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles