Skip to content
All essays
WebMarch 27, 202614 min

SolidJS Stores: Advanced State Management for Complex Applications

Master SolidJS stores for managing complex application state. Learn about createStore, nested updates, immutability, and advanced patterns.

Ü
Ümit Uz
Mobile & Full Stack Developer

While signals are perfect for simple state, SolidJS stores provide powerful tools for managing complex, nested application state. Stores offer fine-grained reactivity with a mutable-like API while maintaining immutability under the hood.

Introduction to Stores

Stores in SolidJS are reactive state containers designed for complex, nested data structures. Unlike signals which hold a single value, stores can hold objects and arrays with deep reactivity.

When to Use Stores

  • Complex Nested State: Objects with multiple levels
  • Collections: Arrays of objects that need individual updates
  • Global State: Application-wide state management
  • Forms: Complex form data with nested fields

Creating Stores

Use createStore to create a reactive store:

tsx
import { createStore } from 'solid-js/store';

interface User {
  id: number;
  name: string;
  email: string;
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
}

const [store, setStore] = createStore<User>({
  id: 1,
  name: 'John Doe',
  email: 'john@example.com',
  preferences: {
    theme: 'light',
    notifications: true,
  },
});

console.log(store.name); // John Doe

Basic Store Operations

Updating Top-Level Properties

tsx
function UserProfile() {
  const [user, setUser] = createStore<User>({
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    preferences: {
      theme: 'light',
      notifications: true,
    },
  });

  const updateName = () => {
    setUser('name', 'Jane Doe');
  };

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={updateName}>Update Name</button>
    </div>
  );
}

Updating Nested Properties

Use path syntax to update nested properties:

tsx
const updateTheme = () => {
  setUser('preferences', 'theme', 'dark');
};

const toggleNotifications = () => {
  setUser('preferences', 'notifications', (notifications) => !notifications);
};

Updating Array Items

tsx
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

function TodoApp() {
  const [todos, setTodos] = createStore<Todo[]>([
    { id: 1, text: 'Learn SolidJS', completed: false },
    { id: 2, text: 'Build app', completed: false },
  ]);

  const toggleTodo = (id: number) => {
    setTodos(
      (todo) => todo.id === id,
      'completed',
      (completed) => !completed
    );
  };

  const updateTodoText = (id: number, newText: string) => {
    setTodos(
      (todo) => todo.id === id,
      'text',
      newText
    );
  };

  return (
    <ul>
      <For each={todos}>
        {(todo) => (
          <li>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
          </li>
        )}
      </For>
    </ul>
  );
}

Advanced Store Operations

Batch Updates

Update multiple properties at once:

tsx
const updateMultiple = () => {
  setStore({
    name: 'Jane Doe',
    email: 'jane@example.com',
  });
};

Using produce for Complex Updates

The produce utility from Immer allows you to write mutable-style code:

tsx
import { produce } from 'solid-js/store';

const addTodo = (text: string) => {
  setTodos(
    produce((todos) => {
      todos.push({
        id: Date.now(),
        text,
        completed: false,
      });
    })
  );
};

const removeTodo = (id: number) => {
  setTodos(
    produce((todos) => {
      const index = todos.findIndex((t) => t.id === id);
      if (index !== -1) {
        todos.splice(index, 1);
      }
    })
  );
};

Merging Updates

Merge multiple updates efficiently:

tsx
const updateUserPreferences = () => {
  setUser('preferences', {
    theme: 'dark',
    notifications: false,
  });
};

Store with Arrays

Appending Items

tsx
const addTodo = (text: string) => {
  setTodos((todos) => [
    ...todos,
    { id: Date.now(), text, completed: false }
  ]);
};

Updating Multiple Items

tsx
const markAllComplete = () => {
  setTodos(
    (todo) => true, // Match all items
    'completed',
    true
  );
};

const markRangeComplete = (start: number, end: number) => {
  setTodos(
    { from: start, to: end }, // Match range
    'completed',
    true
  );
};

Filtering Arrays

tsx
const removeCompleted = () => {
  setTodos((todos) => todos.filter((todo) => !todo.completed));
};

Nested State Management

Deep Nested Updates

tsx
interface AppState {
  user: {
    profile: {
      name: string;
      email: string;
      address: {
        street: string;
        city: string;
        country: string;
      };
    };
    settings: {
      theme: 'light' | 'dark';
      language: string;
    };
  };
}

function App() {
  const [state, setState] = createStore<AppState>({
    user: {
      profile: {
        name: 'John Doe',
        email: 'john@example.com',
        address: {
          street: '123 Main St',
          city: 'New York',
          country: 'USA',
        },
      },
      settings: {
        theme: 'light',
        language: 'en',
      },
    },
  });

  const updateCity = (newCity: string) => {
    setState('user', 'profile', 'address', 'city', newCity);
  };

  const updateTheme = (theme: 'light' | 'dark') => {
    setState('user', 'settings', 'theme', theme);
  };

  return <div>{/* render app */}</div>;
}

Conditional Updates

tsx
const updateIfMatch = (id: number, newText: string) => {
  setTodos(
    (todo) => todo.id === id,
    produce((todo) => {
      if (todo.text.length < 100) {
        todo.text = newText;
      }
    })
  );
};

Store Utilities

Using Arrays

tsx
import { produce } from 'solid-js/store';

function ShoppingList() {
  const [items, setItems] = createStore<ShoppingItem[]>([]);

  const addItem = (name: string, quantity: number) => {
    setItems(
      produce((items) => {
        items.push({ id: Date.now(), name, quantity, purchased: false });
      })
    );
  };

  const updateQuantity = (id: number, quantity: number) => {
    setItems(
      (item) => item.id === id,
      'quantity',
      quantity
    );
  };

  const togglePurchased = (id: number) => {
    setItems(
      (item) => item.id === id,
      'purchased',
      (purchased) => !purchased
    );
  };

  const clearPurchased = () => {
    setItems((items) => items.filter((item) => !item.purchased));
  };

  return (
    <div>
      <For each={items}>
        {(item) => (
          <div>
            <input
              type="checkbox"
              checked={item.purchased}
              onChange={() => togglePurchased(item.id)}
            />
            <span>{item.name}</span>
            <input
              type="number"
              value={item.quantity}
              onInput={(e) => updateQuantity(item.id, parseInt(e.currentTarget.value))}
            />
          </div>
        )}
      </For>
    </div>
  );
}

Using Objects

tsx
function FormState() {
  const [form, setForm] = createStore<FormData>({
    firstName: '',
    lastName: '',
    email: '',
    address: {
      street: '',
      city: '',
      zipCode: '',
    },
  });

  const setFieldValue = (field: string, value: string) => {
    const path = field.split('.');
    if (path.length === 1) {
      setForm(path[0] as keyof FormData, value);
    } else {
      setForm(path[0] as keyof FormData, path[1], value);
    }
  };

  const resetForm = () => {
    setForm({
      firstName: '',
      lastName: '',
      email: '',
      address: {
        street: '',
        city: '',
        zipCode: '',
      },
    });
  };

  return (
    <form>
      <input
        value={form.firstName}
        onInput={(e) => setFieldValue('firstName', e.currentTarget.value)}
      />
      <input
        value={form.lastName}
        onInput={(e) => setFieldValue('lastName', e.currentTarget.value)}
      />
      <input
        value={form.email}
        onInput={(e) => setFieldValue('email', e.currentTarget.value)}
      />
      <input
        value={form.address.street}
        onInput={(e) => setFieldValue('address.street', e.currentTarget.value)}
      />
      <button type="button" onClick={resetForm}>
        Reset
      </button>
    </form>
  );
}

Store Modifiers

readOnly

Create a read-only view of a store:

tsx
import { createStore, produce } from 'solid-js/store';
import { readOnly } from 'solid-js/store';

function ReadOnlyExample() {
  const [store, setStore] = createStore({ count: 0 });
  const readOnlyStore = readOnly(store);

  // This will throw an error
  // readOnlyStore.count = 1;

  return <div>{readOnlyStore.count}</div>;
}

modify

Create a mutable version of a store:

tsx
import { modify } from 'solid-js/store';

const [store, setStore] = createStore({ items: [] });
const mutableStore = modify(store);

// Direct mutations are possible
mutableStore.items.push({ id: 1, name: 'Item' });

Store Patterns

Global Store

Create a global store for app-wide state:

tsx
// store.ts
import { createStore } from 'solid-js/store';

interface AppState {
  user: User | null;
  isAuthenticated: boolean;
  theme: 'light' | 'dark';
}

const [appState, setAppState] = createStore<AppState>({
  user: null,
  isAuthenticated: false,
  theme: 'light',
});

export { appState, setAppState };

Context Store

Share store through context:

tsx
import { createContext, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';

const StoreContext = createContext<ReturnType<typeof createStore>>();

function StoreProvider(props: { children: any }) {
  const [store, setStore] = createStore({ /* initial state */ });
  return (
    <StoreContext.Provider value={[store, setStore]}>
      {props.children}
    </StoreContext.Provider>
  );
}

function useStore() {
  const context = useContext(StoreContext);
  if (!context) throw new Error('StoreContext not found');
  return context;
}

Custom Store Hook

Create reusable store logic:

tsx
function createTodoStore() {
  const [todos, setTodos] = createStore<Todo[]>([]);
  const [filter, setFilter] = createSignal<Filter>('all');

  const addTodo = (text: string) => {
    setTodos((todos) => [
      ...todos,
      { id: Date.now(), text, completed: false }
    ]);
  };

  const toggleTodo = (id: number) => {
    setTodos(
      (todo) => todo.id === id,
      'completed',
      (completed) => !completed
    );
  };

  const removeTodo = (id: number) => {
    setTodos((todos) => todos.filter((todo) => todo.id !== id));
  };

  const filteredTodos = createMemo(() => {
    const currentFilter = filter();
    return todos().filter((todo) => {
      if (currentFilter === 'active') return !todo.completed;
      if (currentFilter === 'completed') return todo.completed;
      return true;
    });
  });

  const stats = createMemo(() => {
    const todosList = todos();
    return {
      total: todosList.length,
      active: todosList.filter((t) => !t.completed).length,
      completed: todosList.filter((t) => t.completed).length,
    };
  });

  return {
    get todos() { return todos(); },
    get filteredTodos() { return filteredTodos(); },
    get stats() { return stats(); },
    get filter() { return filter(); },
    setFilter,
    addTodo,
    toggleTodo,
    removeTodo,
  };
}

Store Persistence

LocalStorage Store

tsx
function createPersistedStore<T>(key: string, initialValue: T) {
  const storedValue = localStorage.getItem(key);
  const initial = storedValue ? JSON.parse(storedValue) : initialValue;

  const [store, setStore] = createStore<T>(initial);

  // Persist to localStorage on changes
  createEffect(() => {
    localStorage.setItem(key, JSON.stringify(store));
  });

  return [store, setStore] as const;
}

// Usage
const [preferences, setPreferences] = createPersistedStore('preferences', {
  theme: 'light',
  language: 'en',
});

SessionStorage Store

tsx
function createSessionStore<T>(key: string, initialValue: T) {
  const storedValue = sessionStorage.getItem(key);
  const initial = storedValue ? JSON.parse(storedValue) : initialValue;

  const [store, setStore] = createStore<T>(initial);

  createEffect(() => {
    sessionStorage.setItem(key, JSON.stringify(store));
  });

  return [store, setStore] as const;
}

Performance Optimization

Shallow Comparison

tsx
const shallowEqual = (a: any, b: any) => {
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);

  if (keysA.length !== keysB.length) return false;

  return keysA.every((key) => a[key] === b[key]);
};

Selective Updates

tsx
const updateSingleField = (id: number, field: string, value: any) => {
  setItems(
    (item) => item.id === id,
    field,
    value
  );
};

Testing Stores

tsx
import { describe, it, expect } from 'vitest';
import { createStore } from 'solid-js/store';

describe('Todo Store', () => {
  it('should add todo', () => {
    const [todos, setTodos] = createStore<Todo[]>([]);
    setTodos((todos) => [
      ...todos,
      { id: 1, text: 'Test', completed: false }
    ]);
    expect(todos.length).toBe(1);
  });

  it('should toggle todo', () => {
    const [todos, setTodos] = createStore<Todo[]>([
      { id: 1, text: 'Test', completed: false }
    ]);

    setTodos(
      (todo) => todo.id === 1,
      'completed',
      (completed) => !completed
    );

    expect(todos[0].completed).toBe(true);
  });
});

Best Practices

  1. 1Use Stores for Complex State: Objects with nested properties
  2. 2Keep Stores Focused: Single responsibility
  3. 3Use TypeScript: Type safety for stores
  4. 4Immutable Updates: Use setters and produce
  5. 5Optimize Updates: Fine-grained updates when possible
  6. 6Test Store Logic: Unit test store operations

Conclusion

SolidJS stores provide a powerful and efficient way to manage complex application state. With fine-grained reactivity, immutable updates, and a mutable-like API, stores make it easy to work with nested data structures while maintaining performance.

By mastering stores, you can build applications with complex state management needs without sacrificing performance or developer experience. The combination of signals and stores gives you the flexibility to handle any state management scenario in SolidJS.

Enjoyed this essay? Share it.

Related essays

Next essay
SolidJS Reactivity: Mastering Signals, Effects, and Reactive Primitives