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.
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:
- First, if there’s a saved preference in
localStorage - 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
useStatefor state management - Use
useEffectfor 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
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
Building a Reusable useFetch Hook in React
Create a custom React hook for data fetching with loading states. Learn how to build reusable hooks that simplify API calls in functional components.
JavaScriptReact Hooks Complete Guide: useState to Custom Hooks
Master all React hooks from basics to advanced. Learn useState, useEffect, useContext, useReducer, useMemo, useCallback, and create custom hooks.
JavaScriptTurborepo: High-Performance Monorepo Build System
Master Turborepo for monorepo management. Learn workspace setup, caching, pipelines, and build performant multi-package JavaScript projects.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.