.claude/skills/solidjs/solid-impl/solid-impl-state-patterns/SKILL.md
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.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer solid-impl-state-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| 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) |
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.
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
| 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 |
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";
// 1. Define state shape
interface AppState {
user: { name: string; email: string } | null;
theme: "light" | "dark";
notifications: { id: string; message: string }[];
}
// 2. Define context type (state + actions)
type AppContextValue = [
state: AppState,
actions: {
setUser: (user: AppState["user"]) => void;
toggleTheme: () => void;
addNotification: (message: string) => void;
removeNotification: (id: string) => void;
}
];
// 3. Create context (no default value — enforce Provider usage)
const AppContext = createContext<AppContextValue>();
// 4. Provider component with store + actions
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>
);
}
// 5. Custom hook with safety check
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>
);
}
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);
// CORRECT: Derived values via createMemo
const subtotal = createMemo(() =>
items().reduce((sum, item) => sum + item.price * item.qty, 0)
);
const tax = createMemo(() => subtotal() * taxRate());
const total = createMemo(() => subtotal() + tax());
// Memos chain: items/taxRate → subtotal → tax → total
// Changing taxRate recalculates tax and total, but NOT subtotal
// WRONG: Using createEffect to sync derived state
const [count, setCount] = createSignal(0);
const [double, setDouble] = createSignal(0);
createEffect(() => {
setDouble(count() * 2); // Unnecessary signal + effect cycle
});
// CORRECT: createMemo — no extra signal, no effect
const double = createMemo(() => count() * 2);
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>
);
}
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>
);
}
ALWAYS create separate contexts for unrelated state domains. This prevents unnecessary re-computations.
// auth-context.tsx
export function AuthProvider(props: ParentProps) { /* ... */ }
export function useAuth() { /* ... */ }
// theme-context.tsx
export function ThemeProvider(props: ParentProps) { /* ... */ }
export function useTheme() { /* ... */ }
// Compose at app root
function App() {
return (
<AuthProvider>
<ThemeProvider>
<Router />
</ThemeProvider>
</AuthProvider>
);
}
import { from } from "solid-js";
// Bridge RxJS observable into SolidJS signal
const currentUser = from(userService.currentUser$);
// Bridge custom subscription
const windowWidth = from((set) => {
const handler = () => set(window.innerWidth);
window.addEventListener("resize", handler);
handler(); // Set initial value
return () => window.removeEventListener("resize", handler);
});
import { createStore, reconcile } from "solid-js/store";
const [state, setState] = createStore({ todos: [] as Todo[] });
// Sync external data — only changed properties trigger updates
websocket.on("todos-updated", (newTodos: Todo[]) => {
setState("todos", reconcile(newTodos));
});
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.