ReactJS 5 min read

Implementing Dark Mode with React Hooks

Learn how to add dark mode functionality to your React application using useState and useEffect hooks with localStorage persistence.

MR

Moshiour Rahman

Advertisement

Overview

Dark mode has become a must-have feature for modern websites and applications. Industry leaders like Twitter, Reddit, and YouTube all support dark mode. It’s more than just a trend - it’s easier on the eyes and perfect for nighttime use.

In this tutorial, you’ll learn how to implement dark mode in a React application using hooks. This is also a practical example of how to use useState and useEffect effectively.

Project Setup

Create a new React application using Create React App:

npx create-react-app react-darkmode-app
cd react-darkmode-app

Note: Make sure you have Node.js installed. Download it from nodejs.org if needed.

Setting Up the Theme Styles

First, let’s create our CSS with light and dark theme classes:

/* src/App.css */

body {
  margin: 0;
  text-align: center;
  transition: background-color 0.3s ease, color 0.3s ease;
}

/* Light Theme */
.light-theme {
  background-color: #ffffff;
  color: #1a1a2e;
}

.light-theme nav {
  background-color: #6366f1;
}

/* Dark Theme */
.dark-theme {
  background-color: #1a1a2e;
  color: #f1f1f1;
}

.dark-theme nav {
  background-color: #16213e;
}

.dark-theme code {
  color: #f472b6;
}

/* Common Styles */
nav {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1rem 2rem;
  color: white;
  transition: background-color 0.3s ease;
}

.content {
  padding: 2rem;
  margin: 0 auto;
  max-width: 600px;
  min-height: 80vh;
}

.theme-toggle {
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  background-color: rgba(255, 255, 255, 0.2);
  color: white;
  transition: all 0.2s ease;
}

.theme-toggle:hover {
  background-color: rgba(255, 255, 255, 0.3);
  transform: translateY(-2px);
}

Persisting User Preference

We want to save the user’s theme preference so it persists across sessions. We’ll use localStorage for this:

// Get the initial theme from localStorage or default to light
const getInitialTheme = () => {
  const savedTheme = localStorage.getItem('theme');
  return savedTheme ? savedTheme === 'dark' : false;
};

The Complete App Component

Here’s the full implementation:

// src/App.jsx
import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  // Initialize state from localStorage
  const getInitialTheme = () => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      return savedTheme === 'dark';
    }
    // Check system preference as fallback
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  };

  const [isDarkMode, setIsDarkMode] = useState(getInitialTheme);

  // Persist theme preference to localStorage
  useEffect(() => {
    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
  }, [isDarkMode]);

  // Toggle function
  const toggleTheme = () => {
    setIsDarkMode(prevMode => !prevMode);
  };

  return (
    <div className={isDarkMode ? 'dark-theme' : 'light-theme'}>
      <nav>
        <button className="theme-toggle" onClick={toggleTheme}>
          {isDarkMode ? '☀️ Light Mode' : '🌙 Dark Mode'}
        </button>
      </nav>

      <div className="content">
        <h1>{isDarkMode ? '🌙 Dark Mode' : '☀️ Light Mode'}</h1>
        <p>
          Click the button above to toggle between light and dark themes.
          Your preference will be saved automatically!
        </p>
        <p>
          Try refreshing the page - your theme choice will persist.
        </p>
      </div>
    </div>
  );
}

export default App;

How It Works

Let’s break down the key parts:

1. State Initialization

const [isDarkMode, setIsDarkMode] = useState(getInitialTheme);

The getInitialTheme function checks:

  1. First, if there’s a saved preference in localStorage
  2. Falls back to the system’s color scheme preference

2. Persisting Changes with useEffect

useEffect(() => {
  localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);

useEffect runs whenever isDarkMode changes, saving the new preference to localStorage.

3. Toggle Function

const toggleTheme = () => {
  setIsDarkMode(prevMode => !prevMode);
};

We use the callback form of setState to toggle based on the previous value. This is safer than directly using !isDarkMode.

4. Conditional Styling

<div className={isDarkMode ? 'dark-theme' : 'light-theme'}>

The theme class is applied based on the current state, triggering our CSS transitions.

Enhanced Version with Custom Hook

For better reusability, extract the logic into a custom hook:

// src/hooks/useTheme.js
import { useState, useEffect } from 'react';

export function useTheme() {
  const getInitialTheme = () => {
    if (typeof window === 'undefined') return false;

    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      return savedTheme === 'dark';
    }

    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  };

  const [isDarkMode, setIsDarkMode] = useState(getInitialTheme);

  useEffect(() => {
    localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');

    // Optional: Update the document class for global styling
    document.documentElement.classList.toggle('dark', isDarkMode);
  }, [isDarkMode]);

  const toggleTheme = () => setIsDarkMode(prev => !prev);

  return { isDarkMode, toggleTheme };
}

Usage:

import { useTheme } from './hooks/useTheme';

function App() {
  const { isDarkMode, toggleTheme } = useTheme();

  return (
    <div className={isDarkMode ? 'dark-theme' : 'light-theme'}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

Conclusion

You’ve learned how to:

  • Implement dark mode using React hooks
  • Persist user preferences with localStorage
  • Use useState for state management
  • Use useEffect for side effects
  • Create a reusable custom hook

Source Code: GitHub Repository

Key Takeaways:

  • Always provide a fallback for first-time users (system preference)
  • Use smooth CSS transitions for a polished experience
  • Extract reusable logic into custom hooks

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.