solid-syntax-context
Quick Reference
Context API Surface
| Function | Import | Purpose |
|---|---|---|
createContext<T>(default?) |
solid-js |
Create a context object with optional default value |
useContext(ctx) |
solid-js |
Read the nearest Provider's value for a context |
<Ctx.Provider value={v}> |
(from context object) | Supply a value to all descendants |
Type Signatures
// createContext
interface Context<T> {
id: symbol;
Provider: (props: { value: T; children: any }) => any;
defaultValue: T;
}
function createContext<T>(): Context<T | undefined>;
function createContext<T>(defaultValue: T): Context<T>;
// useContext
function useContext<T>(context: Context<T>): T;
Critical Warnings
ALWAYS provide a typed generic to createContext<T>() — omitting the generic results in Context<undefined> which provides no type safety for consumers.
ALWAYS wrap useContext in a custom hook with an error check — calling useContext directly gives no feedback when a Provider is missing.
NEVER destructure or access signals/stores when passing them to value — pass the signal accessor or store object directly to preserve reactivity.
NEVER rely on the default value as a functional substitute for a Provider — defaults exist only as a type-narrowing fallback, not as working application state.
ALWAYS define context objects in separate modules — this prevents Hot Module Replacement (HMR) issues where context identity is lost on file reload.
Decision Tree: Context vs Props vs Global Signals
Need to share state between components?
├── Only parent → child (1-2 levels)?
│ └── USE PROPS — simplest, most explicit
├── Deep tree (3+ levels) but single subtree?
│ └── USE CONTEXT — avoids prop drilling, scoped to Provider
├── Multiple unrelated trees need same state?
│ ├── Read-only singleton?
│ │ └── USE MODULE-LEVEL SIGNAL — simplest global state
│ └── Needs encapsulation + methods?
│ └── USE CONTEXT + STORE — full reactive state container
└── Server-provided data needed in deep components?
└── USE CONTEXT — Provider wraps at route level
Core Patterns
1. Basic Context (Typed, with Custom Hook)
This is the recommended pattern for all context usage:
// counter-context.tsx — ALWAYS define in a separate module
import { createContext, useContext, type ParentProps } from "solid-js";
import { createSignal } from "solid-js";
interface CounterState {
count: () => number;
increment: () => void;
decrement: () => void;
}
const CounterContext = createContext<CounterState>();
// ALWAYS export a custom hook — NEVER export the raw context
export function useCounter(): CounterState {
const context = useContext(CounterContext);
if (!context) {
throw new Error("useCounter: must be used within a <CounterProvider>");
}
return context;
}
export function CounterProvider(props: ParentProps) {
const [count, setCount] = createSignal(0);
const value: CounterState = {
count,
increment: () => setCount((c) => c + 1),
decrement: () => setCount((c) => c - 1),
};
return (
<CounterContext.Provider value={value}>
{props.children}
</CounterContext.Provider>
);
}
// app.tsx — usage
import { CounterProvider, useCounter } from "./counter-context";
function Display() {
const { count } = useCounter();
return <p>Count: {count()}</p>;
}
function Controls() {
const { increment, decrement } = useCounter();
return (
<div>
<button
<button
</div>
);
}
export default function App() {
return (
<CounterProvider>
<Display />
<Controls />
</CounterProvider>
);
}
2. Context with Store (Complex State)
import { createContext, useContext, type ParentProps } from "solid-js";
import { createStore, type SetStoreFunction } from "solid-js/store";
interface Todo {
id: number;
text: string;
done: boolean;
}
interface TodoState {
todos: Todo[];
}
type TodoContextValue = [
state: TodoState,
actions: {
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
},
];
const TodoContext = createContext<TodoContextValue>();
export function useTodos(): TodoContextValue {
const context = useContext(TodoContext);
if (!context) {
throw new Error("useTodos: must be used within a <TodoProvider>");
}
return context;
}
export function TodoProvider(props: ParentProps) {
const [state, setState] = createStore<TodoState>({ todos: [] });
const actions = {
addTodo(text: string) {
setState("todos", (prev) => [
...prev,
{ id: Date.now(), text, done: false },
]);
},
toggleTodo(id: number) {
setState("todos", (t) => t.id === id, "done", (d) => !d);
},
};
return (
<TodoContext.Provider value={[state, actions]}>
{props.children}
</TodoContext.Provider>
);
}
3. Nested Context Override
Inner Providers override outer Providers for the same context:
import { ThemeProvider, useTheme } from "./theme-context";
function ThemedBox() {
const { theme } = useTheme();
return <div style={{ background: theme().bg }}>{theme().name}</div>;
}
function App() {
return (
<ThemeProvider theme="light">
<ThemedBox /> {/* reads "light" */}
<ThemeProvider theme="dark">
<ThemedBox /> {/* reads "dark" — inner overrides outer */}
</ThemeProvider>
</ThemeProvider>
);
}
Reactivity Rules
| Pattern | Reactive? | Why |
|---|---|---|
value={{ count: count() }} |
NO | Object is created once at Provider render; count() is read once |
value={{ count }} (passing accessor) |
YES | Accessor function is called in consuming component's render |
value={store} (passing store proxy) |
YES | Store proxy tracks property access in consumers |
value={[state, actions]} (tuple) |
YES | If state is a store or contains accessors |
Rule: ALWAYS pass signal accessors (functions) or store proxies as context values. NEVER call signals inside the value prop object literal — this reads the value once and breaks reactivity.
Reference Links
- references/methods.md — Full type signatures for createContext, useContext, Provider
- references/examples.md — Extended examples: reactive context, typed context, nested contexts, context + stores
- references/anti-patterns.md — Common mistakes: missing Provider, broken reactivity, React contamination