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.