Learn to build a production-ready theme system using the Context API and useReducer. Master global state for light/dark mode in your React dashboard.
Previously in this course, we explored Architecting Global State with Context and Reducer to handle complex data updates. In this lesson, we apply those patterns to a concrete requirement: building a robust, global theme system for our dashboard.
By the end of this lesson, you will be able to create a theme provider, implement a toggle for light/dark modes, and ensure your entire application responds to theme changes instantly.
In a dashboard application, theming isn't just about colors; it's about accessibility, user comfort, and branding. If you pass a theme prop down through every component—from the Layout to the Sidebar to the UserCard—you're creating an unmaintainable mess.
Instead, we want a centralized "Source of Truth" for the current theme. Since we’ve already learned how to avoid prop drilling with Context, we can leverage that pattern to inject theme data into any component that needs it.
We will use a useReducer to manage our theme state. This is superior to useState because it keeps the logic for "toggling" separate from the UI components.
First, we define our action types and the reducer function. This keeps our state transitions predictable.
JAVASCRIPT// ThemeContext.js const themeReducer = (state, action) => { switch (action.type) { case CE9178">'TOGGLE_THEME': return state === CE9178">'light' ? CE9178">'dark' : CE9178">'light'; default: return state; } };
Next, we wrap our state in a Context Provider. This provider will expose both the current theme and the dispatch function (or a cleaner toggleTheme helper).
JAVASCRIPTimport { createContext, useReducer, useContext } from CE9178">'react'; const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, dispatch] = useReducer(themeReducer, CE9178">'light'); const toggleTheme = () => dispatch({ type: CE9178">'TOGGLE_THEME' }); return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> <div className={CE9178">`app-container ${theme}`}> {children} </div> </ThemeContext.Provider> ); };
Now, any component in your dashboard can access the theme without needing props.
JAVASCRIPTconst ThemeToggle = () => { const { theme, toggleTheme } = useContext(ThemeContext); return ( <button onClick={toggleTheme}> Switch to {theme === CE9178">'light' ? CE9178">'Dark' : CE9178">'Light'} Mode </button> ); };
In our running project, let's update the App.js entry point to wrap the entire dashboard in our new ThemeProvider. This ensures that even the deepest child components—like our data charts or settings forms—can adapt to the user's preference.
JAVASCRIPT// App.js function App() { return ( <ThemeProvider> <Navbar /> <main> <DashboardContent /> </main> </ThemeProvider> ); }
By applying the .light or .dark class to our app-container (as shown in the provider code above), we can use CSS variables to handle the actual visual changes:
CSS#9CDCFE">color:#6A9955">/* App.css */ .app-containercolor:#4EC9B0">.light { #9CDCFE">--bg-color: #ffffff; #9CDCFE">--text-color: #000000; } .app-container#9CDCFE">color:#4EC9B0">.dark { #9CDCFE">--bg-color: #1a1a1a; #9CDCFE">--text-color: #ffffff; } #9CDCFE">color:#4EC9B0">body { #9CDCFE">background-color: var(--bg-color); #9CDCFE">color: var(--text-color); }
ThemeContext.js file following the pattern above.ThemeProvider.ThemeToggle button component and place it in your dashboard's Header.value={{ theme, toggleTheme }}), it creates a new object reference on every render. If your ThemeProvider re-renders frequently, this could cause unnecessary re-renders in all consumer components. Fix: Wrap the value in useMemo.useContext(ThemeContext) in a component that isn't a child of ThemeProvider, you'll get undefined. Always provide a default value in createContext() or add a runtime check.localStorage for persistence, you might experience a "flash of light mode" if the initial state is always 'light'. We will cover how to sync this with persistent storage in later lessons.We successfully decoupled our styling from component props by using the Context API. By pairing useReducer with a Context Provider, we've created a clean, testable, and global interface for theme management. This pattern ensures that adding new UI themes in the future won't require a total refactor of your dashboard components.
Up next: Structuring State for Performance, where we will optimize our context usage to prevent unnecessary re-renders in large component trees.
Stop React performance bottlenecks caused by the Context API. Learn how to split contexts, memoize values, and prevent unnecessary re-renders in your app.
Read moreLearn how to use the Context API and useContext to share data across your React application, effectively eliminating prop drilling for cleaner code.
Implementing Theme Context
Dynamic Routing with URL Parameters
Nested Routes and Layouts
Protected Routes for Authenticated Views
Programmatic Navigation
Building the Dashboard Navigation Structure
Asynchronous Data Lifecycle
Caching Strategies with React Query
Mutations and Data Updates
Synchronizing Client and Server State
Integrating Live Data into the Dashboard
Error Handling and Loading UI
Controlled vs Uncontrolled Components
Real-time Form Validation
Schema-based Validation with Zod
Handling Multi-step Forms
Optimizing Form Submissions
Performance Profiling with React DevTools
Refactoring for Scalability
Finalizing Dashboard Data Flow
Deploying the Application
Advanced Hook Composition
Implementing Middleware for State
Advanced Context Patterns
Router Loaders and Data Prefetching
Complex Route Guards
Handling Large Datasets in UI
Testing Hooks and Components
Managing Global Modals
Implementing Keyboard Shortcuts
Optimizing Asset Loading
Internationalization Basics
Managing WebSocket Connections