The React Context API
The Context API is a neat way to pass state across the app without having to use props
TypeScript Masterclass
AVAILABLE NOW with a 50% launch discount!
I’m organining a React Masterclass, I’ll launch it on November 4th, 2025
The Context API was introduced to allow you to pass state (and enable the state to update) across the app, without having to use props for it.
I wrote this article in 2018, but it was using the old components-based React syntax, something no one uses anymore, so today I finally updated it for hooks.
The React team suggests to stick to props if you have just a few levels of children to pass, because it’s still a much less complicated API than the Context API.
In many cases, it enables us to avoid using Redux, simplifying our apps a lot, and also learning how to use React.
How does it work?
You create a context using React.createContext()
, which returns a Context object:
import { createContext } from 'react'
const MyContext = createContext()
The context object contains a Provider
component and a default value. In modern React, we typically use the useContext
hook instead of the Consumer
component.
Then you create a wrapper component that returns a Provider component, and you add as children all the components from which you want to access the context:
import { createContext, useContext, useState } from 'react'
const MyContext = createContext()
function Container({ children }) {
const [something, setSomething] = useState('hey')
return (
<MyContext.Provider value={{ something, setSomething }}>
{children}
</MyContext.Provider>
)
}
function HelloWorld() {
return (
<Container>
<Button />
</Container>
)
}
I used Container as the name of this component because this will be a global provider. You can also create smaller contexts.
Inside a component that’s wrapped in a Provider, you use the useContext hook to access the context:
function Button() {
const { something } = useContext(MyContext)
return <button>{something}</button>
}
You can also pass functions into a Provider value, and those functions will be used by components to update the context state:
function Container({ children }) {
const [something, setSomething] = useState('hey')
const updateSomething = () => setSomething('ho!')
return (
<MyContext.Provider value={{ something, updateSomething }}>
{children}
</MyContext.Provider>
)
}
function Button() {
const { something, updateSomething } = useContext(MyContext)
return (
<button onClick={updateSomething}>
{something}
</button>
)
}
An Example
Here’s a complete example showing how to use Context API with modern React:
import { createContext, useContext, useState } from 'react'
// Create the context
const ThemeContext = createContext()
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Component that uses the context
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Current theme: {theme}
</button>
)
}
// App component
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
)
}
Multiple Contexts Example
In real applications, you’ll often need multiple contexts to manage different aspects of your application state. Here’s an example showing how to use multiple contexts together:
import { createContext, useContext, useState } from 'react'
// Theme Context
const ThemeContext = createContext()
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// User Context
const UserContext = createContext()
function UserProvider({ children }) {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const login = async (credentials) => {
setIsLoading(true)
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
setUser({ name: 'John Doe', email: credentials.email })
setIsLoading(false)
}
const logout = () => setUser(null)
return (
<UserContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</UserContext.Provider>
)
}
// Language Context
const LanguageContext = createContext()
function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en')
const translations = {
en: { welcome: 'Welcome', login: 'Login', logout: 'Logout' },
es: { welcome: 'Bienvenido', login: 'Iniciar sesión', logout: 'Cerrar sesión' }
}
return (
<LanguageContext.Provider value={{ language, setLanguage, t: translations[language] }}>
{children}
</LanguageContext.Provider>
)
}
// Component using multiple contexts
function UserProfile() {
const { theme, toggleTheme } = useContext(ThemeContext)
const { user, logout, isLoading } = useContext(UserContext)
const { t, language, setLanguage } = useContext(LanguageContext)
if (isLoading) return <div>Loading...</div>
return (
<div style={{
padding: '20px',
backgroundColor: theme === 'light' ? '#f5f5f5' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}>
<h2>{t.welcome}, {user?.name || 'Guest'}!</h2>
<div style={{ marginBottom: '10px' }}>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
</div>
<div style={{ marginBottom: '10px' }}>
<button onClick={() => setLanguage(language === 'en' ? 'es' : 'en')}>
Switch to {language === 'en' ? 'Spanish' : 'English'}
</button>
</div>
{user && (
<button onClick={logout}>
{t.logout}
</button>
)}
</div>
)
}
// App with multiple providers
function App() {
return (
<ThemeProvider>
<UserProvider>
<LanguageProvider>
<UserProfile />
</LanguageProvider>
</UserProvider>
</ThemeProvider>
)
}
Why Use Multiple Contexts?
Using multiple contexts is common in real applications for several reasons:
-
Separation of Concerns: Each context handles a specific domain (theme, user authentication, internationalization, etc.)
-
Performance: Components only re-render when the specific context they use changes, not when unrelated state updates
-
Maintainability: It’s easier to manage and debug when each context has a single responsibility
-
Reusability: Contexts can be composed differently in different parts of your app
-
Testing: You can test components with only the contexts they need
-
Team Development: Different developers can work on different contexts without conflicts
The key is to keep each context focused on a single concern and avoid creating one massive context that handles everything in your application.
I wrote 20 books to help you become a better developer:
- JavaScript Handbook
- TypeScript Handbook
- CSS Handbook
- Node.js Handbook
- Astro Handbook
- HTML Handbook
- Next.js Pages Router Handbook
- Alpine.js Handbook
- HTMX Handbook
- React Handbook
- SQL Handbook
- Git Cheat Sheet
- Laravel Handbook
- Express Handbook
- Swift Handbook
- Go Handbook
- PHP Handbook
- Python Handbook
- Linux/Mac CLI Commands Handbook
- C Handbook