| name | solid-impl-state-patterns |
| description | Use when designing state architecture or choosing between signals, stores, and context in SolidJS. Prevents using createEffect for derived state, capturing signal snapshots, and incorrect store mutation patterns. Covers createSignal vs createStore decision matrix, context providers, createMemo for derived state, form state patterns, and state composition strategies. Keywords: createSignal, createStore, createContext, createMemo, global state, context provider, state management, derived state.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires SolidJS 1.x/2.x with TypeScript. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
solid-impl-state-patterns
Quick Reference
State Primitives Overview
| Primitive | Import | Use Case | Access Syntax |
|---|
createSignal | solid-js | Single values, primitives, toggles | signal() (getter function) |
createStore | solid-js/store | Nested objects, arrays, complex state | store.prop (direct access) |
createContext | solid-js | Shared state across component tree | Provider + useContext |
createMemo | solid-js | Derived/computed values | memo() (getter function) |
createMutable | solid-js/store | MobX/Vue migration only | state.prop (direct mutation) |
Critical Warnings
NEVER use createEffect to synchronize derived state — ALWAYS use createMemo instead. Effects are for side effects (DOM, network, logging), not for computing values from other signals.
NEVER store signal values in variables outside tracking scopes — the variable captures a snapshot, not a reactive binding. ALWAYS call the getter function where you need the current value.
NEVER destructure props or stores — destructuring reads values once and breaks reactive tracking. ALWAYS access properties through the proxy object.
NEVER use Redux, useReducer, or Zustand patterns — SolidJS has built-in primitives (signals, stores, context) that are more efficient and idiomatic.
NEVER use global mutable variables for shared state — ALWAYS use createContext + createStore for type-safe, reactive, scoped state sharing.
State Selection Decision Tree
Need state? → What kind of data?
│
├─ Single primitive value (string, number, boolean)
│ → createSignal
│
├─ Object with nested properties / array of items
│ → createStore
│
├─ Derived from other state (computed value)
│ → createMemo (NEVER createEffect + createSignal)
│
├─ Shared across multiple components
│ │
│ ├─ Parent → children (1-2 levels deep)
│ │ → Pass as props (direct)
│ │
│ └─ Across distant components / app-wide
│ → createContext + createStore + Provider
│
├─ Async data from server/API
│ → createResource (SolidJS 1.x) or createAsync (with @solidjs/router)
│
└─ External reactive source (RxJS, custom)
→ from() utility
When to Use Signals vs Stores
| Scenario | Use Signal | Use Store |
|---|
| Counter, toggle, input value | YES | No |
| User profile object | No | YES |
| List of items (todos, products) | No | YES |
| Theme (single string) | YES | No |
| Form with multiple fields | No | YES |
| Nested config object | No | YES |
| Loading/error boolean | YES | No |
Global State Pattern: Context + Store
The canonical SolidJS pattern for shared state combines createContext, createStore, and a Provider component with a custom hook.
import { createContext, useContext, ParentProps } from "solid-js";
import { createStore, SetStoreFunction } from "solid-js/store";
interface AppState {
user: { name: string; email: string } | null;
theme: "light" | "dark";
notifications: { id: string; message: string }[];
}
type AppContextValue = [
state: AppState,
actions: {
setUser: (user: AppState["user"]) => void;
toggleTheme: () => void;
addNotification: (message: string) => void;
removeNotification: (id: string) => void;
}
];
const AppContext = createContext<AppContextValue>();
export function AppProvider(props: ParentProps) {
const [state, setState] = createStore<AppState>({
user: null,
theme: "light",
notifications: [],
});
const actions = {
setUser: (user: AppState["user"]) => setState("user", user),
toggleTheme: () =>
setState("theme", (prev) => (prev === "light" ? "dark" : "light")),
addNotification: (message: string) =>
setState("notifications", (prev) => [
...prev,
{ id: crypto.randomUUID(), message },
]),
removeNotification: (id: string) =>
setState("notifications", (prev) => prev.filter((n) => n.id !== id)),
};
return (
<AppContext.Provider value={[state, actions]}>
{props.children}
</AppContext.Provider>
);
}
export function useApp() {
const context = useContext(AppContext);
if (!context) throw new Error("useApp must be used within AppProvider");
return context;
}
Usage in components:
function Header() {
const [state, { toggleTheme }] = useApp();
return (
<header>
<span>{state.user?.name ?? "Guest"}</span>
<button onClick={toggleTheme}>Theme: {state.theme}</button>
</header>
);
}
Derived State with createMemo
ALWAYS use createMemo for values computed from other reactive state. Memos cache results and only recompute when dependencies change.
import { createSignal, createMemo } from "solid-js";
const [items, setItems] = createSignal<{ price: number; qty: number }[]>([]);
const [taxRate, setTaxRate] = createSignal(0.21);
const subtotal = createMemo(() =>
items().reduce((sum, item) => sum + item.price * item.qty, 0)
);
const tax = createMemo(() => subtotal() * taxRate());
const total = createMemo(() => subtotal() + tax());
Derived State Anti-Pattern
const [count, setCount] = createSignal(0);
const [double, setDouble] = createSignal(0);
createEffect(() => {
setDouble(count() * 2);
});
const double = createMemo(() => count() * 2);
Form State Pattern
Store-Based Form (Recommended for Multi-Field Forms)
import { createStore } from "solid-js/store";
import { createSignal, Show } from "solid-js";
interface FormData {
name: string;
email: string;
role: "admin" | "user" | "viewer";
}
interface FormErrors {
name?: string;
email?: string;
}
function UserForm() {
const [form, setForm] = createStore<FormData>({
name: "",
email: "",
role: "user",
});
const [errors, setErrors] = createStore<FormErrors>({});
const [submitting, setSubmitting] = createSignal(false);
const validate = (): boolean => {
const newErrors: FormErrors = {};
if (!form.name.trim()) newErrors.name = "Name is required";
if (!form.email.includes("@")) newErrors.email = "Invalid email";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
if (!validate()) return;
setSubmitting(true);
try {
await saveUser(form);
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={form.name}
onInput={(e) => setForm("name", e.currentTarget.value)}
/>
<Show when={errors.name}><span class="error">{errors.name}</span></Show>
<input
value={form.email}
onInput={(e) => setForm("email", e.currentTarget.value)}
/>
<Show when={errors.email}><span class="error">{errors.email}</span></Show>
<select
value={form.role}
onChange={(e) => setForm("role", e.currentTarget.value as FormData["role"])}
>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="viewer">Viewer</option>
</select>
<button type="submit" disabled={submitting()}>
{submitting() ? "Saving..." : "Save"}
</button>
</form>
);
}
State Composition Strategies
splitProps for Prop Separation
import { splitProps } from "solid-js";
function Button(props: {
variant: "primary" | "secondary";
size: "sm" | "md" | "lg";
onClick: () => void;
children: any;
class?: string;
disabled?: boolean;
}) {
const [local, others] = splitProps(props, ["variant", "size", "children"]);
return (
<button
{...others}
class={`btn btn-${local.variant} btn-${local.size} ${others.class ?? ""}`}
>
{local.children}
</button>
);
}
Multiple Contexts (Feature Slicing)
ALWAYS create separate contexts for unrelated state domains. This prevents unnecessary re-computations.
export function AuthProvider(props: ParentProps) { }
export function useAuth() { }
export function ThemeProvider(props: ParentProps) { }
export function useTheme() { }
function App() {
return (
<AuthProvider>
<ThemeProvider>
<Router />
</ThemeProvider>
</AuthProvider>
);
}
External State Integration with from()
import { from } from "solid-js";
const currentUser = from(userService.currentUser$);
const windowWidth = from((set) => {
const handler = () => set(window.innerWidth);
window.addEventListener("resize", handler);
handler();
return () => window.removeEventListener("resize", handler);
});
reconcile() for External Data Sync
import { createStore, reconcile } from "solid-js/store";
const [state, setState] = createStore({ todos: [] as Todo[] });
websocket.on("todos-updated", (newTodos: Todo[]) => {
setState("todos", reconcile(newTodos));
});
Reference Links
Official Sources