Table of Contents
Hey! So, you know how dark mode is everywhere now, right? It’s super helpful for users, especially when browsing in the evening or in low light. I thought it would be fun to show you how to add a dark mode toggle to a React app using Jotai for managing the state and a custom hook for organizing the logic. It’s not too hard once you break it down!
Steps To Build Dark Mode with React, Jotai, and Custom Hooks
Step 1: Using Jotai for State Management
So, for managing the state, I went with Jotai—it’s a state management library for React that a friend suggested when I was ranting about how complicated Redux is. Jotai just makes sense to me because it’s way simpler. Let me show you how I set it up:
// src/store/uiAtoms.ts
import { atom } from "jotai"
import { atomWithStorage } from "jotai/utils"
import type { ThemeMode, UIState } from "@/types"
export const uiStateAtom = atomWithStorage<UIState>("uiState", {
darkMode: "system",
showCompleted: false, // other UI stuff
})
export const setDarkModeAtom = atom(null, (get, set, mode: ThemeMode) => {
const currentState = get(uiStateAtom)
set(uiStateAtom, { ...currentState, darkMode: mode })
})
Here, I’m using atomWithStorage
, which basically saves the user’s theme preference (light, dark, or system) to local storage so it sticks between page reloads. I used to write my own logic for this before I discovered Jotai does it for me — classic case of reinventing the wheel!
Step 2: Creating a Custom Hook for the UI
Next, I made a custom hook. I find it cleaner to pull out all the UI logic and state management into its own hook so I can easily reuse it in other components.
// src/hooks/useUI.ts
import { useAtom } from "jotai"
import { uiStateAtom, setDarkModeAtom } from "@/store/uiAtoms"
export const useUI = () => {
const [uiState] = useAtom(uiStateAtom)
const [, setDarkMode] = useAtom(setDarkModeAtom)
return {
uiState,
setDarkMode,
}
}
This hook is super simple but effective. It gives us access to the current UI state and a function to change the dark mode, which is exactly what we need.
Step 3: Building the DarkModeToggle Component
Now, for the fun part—the actual toggle component. I ended up using a pre-built dropdown menu from a library called shadcn (which is based on Radix, a great choice if you care about accessibility).
// src/components/common/DarkModeToggle.tsx
import React, { useEffect } from "react";
import { useUI } from "@/hooks/useUI";
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
const DarkModeToggle: React.FC<{ className?: string }> = ({ className }) => {
const { setDarkMode, uiState } = useUI();
useEffect(() => {
const root = document.documentElement;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (uiState.darkMode === "dark" || (uiState.darkMode === "system" && prefersDark)) {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
}, [uiState.darkMode]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className={className}>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setDarkMode("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDarkMode("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDarkMode("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default DarkModeToggle;
This component handles the UI for the dark mode toggle. The useEffect
bit listens for changes in the user’s system preferences and applies the right class to the root of the document. This allows the dark mode to actually toggle when the user clicks the button.
Putting It All Together
So, with all the pieces in place, the process is straightforward:
- Jotai stores the current dark mode state.
- The useUI hook makes it easy to read and update the dark mode.
- The DarkModeToggle component provides the UI for switching between light, dark, and system modes.
That’s it! Not only does this solution store the user’s preference, but it also respects the system’s default theme settings if you go with the “system” option. And with Jotai, you get a nice, clean state management experience.
Conclusion
Dark mode might seem like a small feature, but it’s one of those things that can really improve the user experience. Plus, it’s super satisfying to build. I personally love using custom hooks to keep my code organized and reusable, and this is a great example of how they make the whole process smoother.