Back to Blog
Lesson 12 of the Intermediate React: Hooks, State & Data Patterns course
ReactJune 25, 20264 min read

Implementing Theme Context: A Global Toggle for React Dashboards

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.

ReactHooksContext APIReducerStylingUIjavascriptfrontend

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.

The Problem with Prop-Drilled Styles

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.

Designing the Theme Architecture

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.

1. Define the Reducer Logic

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;
  }
};

2. Create the Provider

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).

JAVASCRIPT
import { 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>
  );
};

3. Consume the Theme

Now, any component in your dashboard can access the theme without needing props.

JAVASCRIPT
const ThemeToggle = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button onClick={toggleTheme}>
      Switch to {theme === CE9178">'light' ? CE9178">'Dark' : CE9178">'Light'} Mode
    </button>
  );
};

Worked Example: Integrating into the Dashboard

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);
}

Hands-on Exercise

  1. Create a ThemeContext.js file following the pattern above.
  2. Wrap your root application component with the ThemeProvider.
  3. Create a ThemeToggle button component and place it in your dashboard's Header.
  4. Verify that clicking the button switches the CSS class on the main container and updates the UI colors.

Common Pitfalls

  • Re-rendering the whole tree: If your provider value is an object literal (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.
  • Missing Provider: If you try to call 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.
  • Flickering: If you rely on 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.

Recap

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.

Similar Posts