Context
Share state across your component tree without passing props through every level.
The Prop Drilling Problem
Imagine you have a theme that many components need:
// Without context, you pass theme through every level
function App() {
const theme = signal('dark');
return <Layout theme={theme} />;
}
function Layout({ theme }) {
return <Sidebar theme={theme} />; // just passing it through
}
function Sidebar({ theme }) {
return <Menu theme={theme} />; // still just passing
}
function Menu({ theme }) {
return <MenuItem theme={theme} />; // ugh, more passing
}
function MenuItem({ theme }) {
// Finally! We actually use it
return <span className={theme}>...</span>;
}
This is called prop drilling. Context solves it.
Creating Context
Create a context with a default value:
import { createContext } from 'what-framework';
// Create the context
const ThemeContext = createContext('light');
Providing Context
Wrap your tree with the context provider:
import { signal } from 'what-framework';
function App() {
const theme = signal('dark');
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
);
}
Consuming Context
Access the value anywhere in the tree:
import { useContext } from 'what-framework';
function MenuItem() {
const theme = useContext(ThemeContext);
return (
<span className={() => theme()}>
Menu Item
</span>
);
}
No more prop drilling! Components in between don't need to know about theme at all.
Complete Example
import { createContext, signal } from 'what-framework';
// Create context with default
export const ThemeContext = createContext(null);
// Provider component with state and actions
export function ThemeProvider({ children }) {
const theme = signal('light');
const value = {
theme,
toggle: () => theme.set(t => t === 'light' ? 'dark' : 'light'),
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
import { useContext } from 'what-framework';
import { ThemeContext } from '../context/theme';
export function ThemeToggle() {
const { theme, toggle } = useContext(ThemeContext);
return (
<button onClick={toggle}>
Current: {theme}
</button>
);
}
import { ThemeProvider } from './context/theme';
import { ThemeToggle } from './components/ThemeToggle';
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
);
}
function Header() {
return (
<header>
<ThemeToggle />
</header>
);
}
Nested Providers
You can nest providers to override values in subtrees:
<ThemeContext.Provider value={'light'}>
<Header /> {/* uses 'light' */}
<ThemeContext.Provider value={'dark'}>
<Sidebar /> {/* uses 'dark' */}
</ThemeContext.Provider>
<Footer /> {/* uses 'light' */}
</ThemeContext.Provider>
Common Patterns
Auth Context
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const user = signal(null);
const isLoggedIn = computed(() => user() !== null);
const login = async (credentials) => {
const userData = await api.login(credentials);
user.set(userData);
};
const logout = () => user.set(null);
return (
<AuthContext.Provider value={{ user, isLoggedIn, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Custom Hook Pattern
// Create a custom hook for cleaner usage
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage
function Profile() {
const { user, logout } = useAuth();
// ...
}
Alternative: Stores
For global state that doesn't need the component tree hierarchy, createStore provides a simpler pattern. Use derived() to mark computed properties (this distinguishes them from actions):
import { createStore, derived } from 'what-framework';
const useCounter = createStore({
// State (plain values become signals)
count: 0,
// Computed (must use derived marker)
doubled: derived(state => state.count * 2),
// Actions (plain functions, use `this`)
increment() { this.count++; },
decrement() { this.count--; },
});
// Use anywhere in your app — no Provider needed
function Counter() {
const { count, doubled, increment } = useCounter();
return (
<div>
<p>Count: {count()} / Doubled: {doubled()}</p>
<button onClick={increment}>+</button>
</div>
);
}
Why derived()?
The derived() marker is required so that createStore can distinguish computed properties from actions. Without it, a function like state => state.count * 2 looks the same as an action like item => { ... }. The marker removes this ambiguity.
Best Practices
- Don't overuse context. Props are simpler for 1-2 levels of nesting.
- Split contexts by concern. Don't put unrelated state in one context.
- Provide signals, not values. Pass signals so consumers can subscribe to updates.
- Create custom hooks. Hide context access behind a hook for better DX.