Managing global state in a React application is a common requirement, and using the Context API combined with Hooks like useContext
and useReducer
provides a powerful and efficient way to achieve this without adding external dependencies like Redux.
We'll walk through creating a theme toggler that switches between light and dark modes across the entire application. This example will demonstrate how to set up and use global state management with React Context and Hooks.
Overview
Concepts we'll cover:
- Context API: Allows us to create a context (a global variable) that can be accessed by any component in the component tree without prop drilling.
- useReducer Hook: Manages complex state logic by providing a reducer function and an initial state.
- useContext Hook: Provides easy access to the context value in functional components.
Structure we'll build:
- ThemeContext: Holds the current theme state and a dispatch function to toggle the theme.
- ThemeProvider: Wraps the application and provides the theme context to all components.
- Themed Components: Consume the theme context to display and toggle themes.
Step-by-Step Implementation
1. Initialize a React Application
First, ensure you have a React application set up. You can create one using Create React App:
npx create-react-app react-context-example
cd react-context-example
npm start
This sets up a basic React application ready for development.
2. Create the Theme Context
We'll start by creating a ThemeContext
that will hold our theme state and provide it to the rest of the app.
File Structure:
src/
|-- contexts/
| |-- ThemeContext.js
|-- components/
| |-- ThemeToggler.js
| |-- Header.js
| |-- Content.js
|-- App.js
src/contexts/ThemeContext.js
:
import React, { createContext, useReducer } from 'react';
// Define initial state
const initialState = {
isDarkTheme: false,
};
// Create context
export const ThemeContext = createContext();
// Define reducer
const themeReducer = (state, action) => {
switch (action.type) {
case 'TOGGLE_THEME':
return { isDarkTheme: !state.isDarkTheme };
default:
return state;
}
};
// Create provider component
export const ThemeProvider = ({ children }) => {
const [state, dispatch] = useReducer(themeReducer, initialState);
const value = { state, dispatch };
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
Explanation:
createContext
: Initializes a new context.useReducer
: Manages the theme state with a reducer function.themeReducer
: Toggles theisDarkTheme
boolean based on the dispatched action.ThemeProvider
: Wraps around components that need access to the theme context.
3. Wrap the App with ThemeProvider
Now, we'll wrap our main application with the ThemeProvider
so that all child components can access the theme context.
src/App.js
:
import React from 'react';
import { ThemeProvider } from './contexts/ThemeContext';
import Header from './components/Header';
import Content from './components/Content';
import ThemeToggler from './components/ThemeToggler';
function App() {
return (
<ThemeProvider>
<div>
<Header />
<Content />
<ThemeToggler />
</div>
</ThemeProvider>
);
}
export default App;
Explanation:
- The entire application is wrapped inside
ThemeProvider
. - Components like
Header
,Content
, andThemeToggler
can now access and modify the theme state.
4. Create Themed Components
Let's create components that consume the theme context and adjust their styles accordingly.
src/components/Header.js
:
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
const Header = () => {
const { state } = useContext(ThemeContext);
const headerStyle = {
padding: '10px',
textAlign: 'center',
backgroundColor: state.isDarkTheme ? '#333' : '#eee',
color: state.isDarkTheme ? '#eee' : '#333',
};
return <header style={headerStyle}>My Themed App</header>;
};
export default Header;
src/components/Content.js
:
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
const Content = () => {
const { state } = useContext(ThemeContext);
const contentStyle = {
padding: '20px',
backgroundColor: state.isDarkTheme ? '#555' : '#fff',
color: state.isDarkTheme ? '#fff' : '#000',
minHeight: '200px',
};
return (
<main style={contentStyle}>
<p>This is some content that reflects the current theme!</p>
</main>
);
};
export default Content;
src/components/ThemeToggler.js
:
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
const ThemeToggler = () => {
const { state, dispatch } = useContext(ThemeContext);
const buttonStyle = {
padding: '10px 20px',
margin: '20px auto',
display: 'block',
backgroundColor: state.isDarkTheme ? '#999' : '#444',
color: '#fff',
border: 'none',
cursor: 'pointer',
};
const toggleTheme = () => {
dispatch({ type: 'TOGGLE_THEME' });
};
return (
<button style={buttonStyle} onClick={toggleTheme}>
Toggle to {state.isDarkTheme ? 'Light' : 'Dark'} Theme
</button>
);
};
export default ThemeToggler;
Explanation:
useContext
Hook: Each component usesuseContext
to access the current theme state.- Dynamic Styles: Styles are adjusted based on
isDarkTheme
. ThemeToggler
: Dispatches aTOGGLE_THEME
action to switch themes.
5. Running the Application
Start the application with:
npm start
Result:
- The app displays a header, content area, and a button to toggle the theme.
- Clicking the Toggle Theme button switches between light and dark themes globally.
Detailed Explanation
Why Use Context and Hooks?
-
Context API:
- Eliminates the need for prop drilling by providing a way to pass data through the component tree directly.
- Suitable for global settings like themes, user authentication, and language preferences.
-
useReducer Hook:
- Manages complex state logic and state transitions.
- Provides a predictable way to update state based on dispatched actions.
- Makes state management more scalable and maintainable compared to multiple
useState
hooks.
-
useContext Hook:
- Simplifies consuming context values in functional components.
- Provides a straightforward API to access and use context data.
Benefits of This Approach
-
Scalability:
- Easy to add more global states and actions by extending the reducer and context.
- Supports complex state updates without cluttering components.
-
Maintainability:
- Centralizes state management logic, making it easier to debug and maintain.
- Components remain clean and focused on UI rendering.
-
Performance:
- React ensures efficient re-renders by updating only components that consume the context.
- Avoids unnecessary prop passing and state lifting.
Extending the Example
You can extend this setup by:
-
Adding More Themes:
- Update the state to include different theme options and modify the reducer to handle multiple themes.
-
Persisting Theme Preference:
- Use
localStorage
to save and retrieve the user's theme preference across sessions.
- Use
-
Handling Multiple Global States:
- Create additional contexts for other global states like user authentication, language settings, etc.
-
Creating Custom Hooks:
- Abstract common logic into custom hooks for reusability and cleaner code.
Conclusion
Using React's Context API combined with Hooks like useContext
and useReducer
provides a robust and efficient way to manage global state in your applications. This approach is built-in, lightweight, and avoids the need for external state management libraries unless your application's complexity demands it.
By following the structure and examples provided, you can implement flexible and maintainable global state management tailored to your application's needs.