Preact Context — Sharing State Across the Component Tree
In this tutorial, you will learn about Preact Context. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Preact Context API to share state across components without prop drilling, using createContext and useContext in the 3kB Preact framework.
In this lesson, you'll understand how to create context, provide values, and consume them in any descendant component without passing props through intermediaries.
What You'll Learn
How to use createContext to define shared state, Provider to supply values, and useContext to consume them in functional components.
Why It Matters
Props-only data flow requires every intermediate component to forward props, even if they don't use them. Context eliminates this boilerplate and makes deep data sharing clean.
Real-World Use
Doda Browser's extension uses context to share the current user session, theme preference, and language setting across 20+ components without threading props through every level.
flowchart TD
A[App Provider] -->|Theme Context| B[Header]
A -->|Theme Context| C[Sidebar]
A -->|Theme Context| D[Main Content]
A -->|Theme Context| E[Footer]
B --> F[useContext Theme]
C --> G[useContext Theme]
D --> H[useContext Theme]
style A fill:#673ab8,color:#fff
style F fill:#4a148c,color:#fff
style H fill:#4a148c,color:#fff
Creating Context
Define context with a default value:
import { createContext } from 'preact';
const ThemeContext = createContext('light');
const UserContext = createContext(null);
The argument to createContext is the default value used when a component reads context outside a Provider.
Providing Context
Wrap the component tree with a Provider:
import { createContext } from 'preact';
import { useState } from 'preact/hooks';
const ThemeContext = createContext('light');
const UserContext = createContext(null);
function App() {
const [theme, setTheme] = useState('light');
const user = { name: 'Alice', role: 'admin' };
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={user}>
<Header />
<MainContent />
<Footer />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
Output: Any component inside the ThemeContext.Provider and UserContext.Provider can access theme and user data without receiving them as props.
Consuming Context with useContext
Access context values in functional components:
import { useContext } from 'preact/hooks';
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
const user = useContext(UserContext);
return (
<header style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<span>Welcome, {user?.name}</span>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
</header>
);
}
Output: The header shows the user's name and a theme toggle button. Changing the theme updates the header's colors and any other component consuming ThemeContext.
Context with Signals
Combine context with signals for efficient updates:
import { createContext } from 'preact';
import { signal } from '@preact/signals';
const CartContext = createContext();
function CartProvider({ children }) {
const cart = signal([]);
const total = computed(() =>
cart.value.reduce((sum, item) => sum + item.price * item.qty, 0)
);
const addItem = (item) => {
cart.value = [...cart.value, { ...item, qty: 1 }];
};
const removeItem = (id) => {
cart.value = cart.value.filter(item => item.id !== id);
};
return (
<CartContext.Provider value={{ cart, total, addItem, removeItem }}>
{children}
</CartContext.Provider>
);
}
function CartBadge() {
const { cart } = useContext(CartContext);
// Only the badge text updates when cart changes — no re-render
return <span>Cart: {cart.value.length} items</span>;
}
Output: The cart badge reads a signal from context. When items are added or removed, only the badge text node updates, not the entire component.
Multiple Contexts
Components can consume multiple contexts:
const ThemeContext = createContext('light');
const LanguageContext = createContext('en');
const AuthContext = createContext(null);
function SettingsPanel() {
const { theme, setTheme } = useContext(ThemeContext);
const { lang, setLang } = useContext(LanguageContext);
const user = useContext(AuthContext);
if (!user) return <p>Please log in to change settings.</p>;
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={lang} onChange={e => setLang(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
}
Output: The settings panel reads theme, language, and auth context simultaneously. Changes to any context update only the relevant parts.
Context vs Prop Drilling
Compare the two approaches:
// Prop drilling — every intermediate component must forward props
function App() { return <Header user={user} theme={theme} />; }
function Header({ user, theme }) { return <Nav user={user} theme={theme} />; }
function Nav({ user, theme }) { return <Avatar user={user} theme={theme} />; }
function Avatar({ user, theme }) { return <img src={user.avatar} className={theme} />; }
// Context — only the consuming component needs the data
function Avatar() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
return <img src={user.avatar} className={theme} />;
}
Output: With context, the Avatar component directly accesses user and theme without Header and Nav having to forward props they don't use.
Common Mistakes
- Putting everything in one context: Splitting unrelated state (theme, auth, cart) into separate contexts prevents unnecessary re-renders when one part changes.
- Creating a new context value every render:
value={{ theme, setTheme }}creates a new object every render, causing all consumers to re-render. UseuseMemoor signals. - Using context for localized state: Context is for state shared across many components. Local component state should stay in
useStateoruseReducer. - Reading context outside a Provider: Components that read context without a parent Provider get the default value. This can hide bugs if the default isn't meaningful.
- Overusing context: Not everything needs context. If data only flows through 2-3 levels, prop drilling is simpler and more explicit.
Practice Questions
What does
createContextreturn? Answer: A context object withProviderandConsumerproperties. TheProviderwraps the component tree to supply values.How do you read context in a functional component? Answer: Call
useContext(ContextObject)frompreact/hooks. It returns the current context value.What happens if you read context without a matching Provider? Answer: The component gets the default value passed to
createContext(defaultValue).Why should unrelated state be in separate contexts? Answer: When a context value changes, all consumers re-render. Separate contexts prevent unrelated state changes from triggering unnecessary re-renders.
Challenge
Create a multi-language (i18n) system using context: a LanguageContext provides the current language and a t(key) translation function. Create a nested component tree that demonstrates translation without prop drilling.
Mini Project
Build a user authentication system with context: AuthContext provides user data, login/logout functions, and loading state. Protect certain routes by consuming the auth context in a route guard component.
FAQ
What's Next
Learn about Preact Refs and DOM Access to understand how to directly access DOM elements and integrate with imperative APIs.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro