Building a Reusable Dark Mode Toggle in React: A Line-by-Line Code Walkthrough

5 min read

📚 Quick Review: This practical application is built upon a fundamental programming concept. Review the Theory Lesson here first.


Demystifying useDarkMode: A Practical Code Breakdown

Understanding the theory behind custom hooks is one thing; seeing them in action and dissecting their implementation is another. In this practical lesson, we’ll dive deep into the provided useDarkMode custom hook, breaking down each line of code to understand its purpose, how it leverages React’s core features, and its interaction with the browser’s execution environment. By the end, you’ll have a solid grasp of how to build and integrate such a powerful utility into your own React applications.

Prerequisites

Before we begin, a basic understanding of React, functional components, and the fundamental concepts of useState and useEffect hooks will be beneficial.

The useDarkMode Custom Hook: Full Code

Here’s the complete code snippet we’ll be analyzing:

import { useState, useEffect } from 'react';function useDarkMode() {  const [theme, setTheme] = useState('light');  useEffect(() => {    // Check for saved preference or system settings    const savedTheme = window.localStorage.getItem('theme');    if (savedTheme) {      setTheme(savedTheme);    } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {      setTheme('dark');    }  }, []);  useEffect(() => {    // Update document object and save preference    const root = window.document.documentElement;    root.classList.remove('light', 'dark');    root.classList.add(theme);    window.localStorage.setItem('theme', theme);  }, [theme]);  const toggleTheme = () => {    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));  };  return [theme, toggleTheme];}export default useDarkMode;

Line-by-Line Code Breakdown

1. Importing Hooks

import { useState, useEffect } from 'react';

This line imports the necessary React Hooks: useState for managing component state and useEffect for performing side effects.

2. Defining the Custom Hook

function useDarkMode() {

This defines our custom hook, named useDarkMode. By convention, all custom hooks start with the prefix “use” to signal that they follow the Rules of Hooks.

3. Initializing Theme State

  const [theme, setTheme] = useState('light');

Here, we use useState to declare a state variable named theme and its corresponding setter function setTheme. The initial value of theme is set to 'light'. This theme variable will hold the current active theme (‘light’ or ‘dark’).

4. First useEffect: Initial Theme Detection

  useEffect(() => {    // Check for saved preference or system settings    const savedTheme = window.localStorage.getItem('theme');    if (savedTheme) {      setTheme(savedTheme);    } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {      setTheme('dark');    }  }, []);

This useEffect hook is crucial for initializing the theme when the component (or the hook consumer) first mounts. Let’s break down its logic:

  • const savedTheme = window.localStorage.getItem('theme');: It first attempts to retrieve a previously saved theme preference from the browser’s localStorage. This ensures user preferences are remembered across sessions.
  • if (savedTheme) { setTheme(savedTheme); }: If a saved theme is found, it updates the theme state with that value.
  • else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { setTheme('dark'); }: If no saved theme is found, it then checks the user’s system preference using window.matchMedia('(prefers-color-scheme: dark)'). If the system prefers dark mode, the theme is set to 'dark'.
  • }, []);: The empty dependency array [] ensures this useEffect runs only once after the initial render, mimicking componentDidMount behavior in class components.

5. Second useEffect: Applying and Persisting Theme Changes

  useEffect(() => {    // Update document object and save preference    const root = window.document.documentElement;    root.classList.remove('light', 'dark');    root.classList.add(theme);    window.localStorage.setItem('theme', theme);  }, [theme]);

This useEffect hook handles the actual application of the theme to the DOM and its persistence. It runs whenever the theme state changes:

  • const root = window.document.documentElement;: It gets a reference to the HTML root element (<html>). This is where we’ll apply our theme classes.
  • root.classList.remove('light', 'dark');: It removes any existing ‘light’ or ‘dark’ classes from the root element to ensure a clean slate before applying the new theme.
  • root.classList.add(theme);: It adds the current theme (either ‘light’ or ‘dark’) as a class to the HTML root element. Your CSS can then target these classes (e.g., html.dark { /* dark mode styles */ }).
  • window.localStorage.setItem('theme', theme);: It saves the current theme to localStorage, so the preference is remembered for future visits.
  • }, [theme]);: The dependency array [theme] means this useEffect will re-run whenever the theme state variable changes.

6. Defining the Theme Toggle Function

  const toggleTheme = () => {    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));  };

This function, toggleTheme, provides the mechanism for users to switch between themes. It uses the setTheme updater function with a callback to get the prevTheme and then sets the theme to its opposite.

7. Returning Hook Values

  return [theme, toggleTheme];

Finally, the useDarkMode hook returns an array containing the current theme state and the toggleTheme function. This allows any component that uses useDarkMode to access these values and functions.

8. Exporting the Hook

export default useDarkMode;

This line makes the useDarkMode custom hook available for import and use in other React components throughout your application.

Execution Environment and Browser APIs

The useDarkMode hook heavily relies on several Browser APIs to function correctly within a web environment:

  • window.localStorage: Used for client-side data storage, allowing the application to remember the user’s theme preference across browser sessions.
  • window.matchMedia(): A powerful API for querying the document about the characteristics of the display device. Here, it’s used to detect the user’s operating system’s preferred color scheme.
  • window.document.documentElement: This property returns the <html> element of the document. By adding and removing classes on this element, the entire application’s styling can be dynamically changed via CSS.

These APIs are fundamental to how the hook interacts with the browser and persists user settings, making it a robust solution for dynamic theming.

💡 Developer Tip: While useEffect is powerful, be cautious with its dependency array. An incorrect dependency array (e.g., missing a dependency or including too many) can lead to infinite loops or stale closures. Always ensure your dependencies accurately reflect all values from the component scope that your effect uses.

Conclusion

The useDarkMode custom hook is a prime example of how React Hooks enable developers to write cleaner, more modular, and reusable code. By encapsulating the complex logic of theme detection, application, and persistence, it provides a simple and elegant API for any component to consume. This line-by-line breakdown should equip you with the knowledge to not only use this hook effectively but also to inspire you to create your own custom hooks for common patterns in your React projects.

Leave a Reply

Your email address will not be published. Required fields are marked *