Guide to implementing theme switching (light/dark) using React Context, HOC, and localStorage.
Benefit: Isolation of theme logic from business components. Automatic saving of user choice and prevention of theme 'flicker' on load.
Located in src/app/providers/theme/. Responsible for global initialization.
Context, hooks, and UI components are placed in shared/ as they are reusable.
light, dark) and context structure for state management.1export type TTheme = "light" | "dark";
2
3export interface IThemeContextType {
4 theme: TTheme;
5 toggleTheme: () => void;
6}1import { createContext } from "react";
2import { IThemeContextType } from "./theme.types";
3
4export const ThemeContext = createContext<IThemeContextType | undefined>(undefined);useTheme hook with provider presence validation.1import { useContext } from "react";
2import { ThemeContext } from "../context/theme.context";
3
4export const useTheme = () => {
5 const context = useContext(ThemeContext);
6 if (!context) {
7 throw new Error("useTheme must be used within a ThemeProvider");
8 }
9 return context;
10};localStorage, and manipulates html classes.1import React, { useCallback, useEffect, useMemo, useState } from "react";
2import { type TTheme, ThemeContext } from "@/shared/ui/layout/theme-toggle/model";
3
4export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
5 const [theme, setTheme] = useState<TTheme>("light");
6 const [mounted, setMounted] = useState(false);
7
8 useEffect(() => {
9 const saved = localStorage.getItem("theme") as TTheme;
10 if (saved) setTheme(saved);
11 setMounted(true);
12 }, []);
13
14 const toggleTheme = useCallback(() => {
15 setTheme((prev) => {
16 const next = prev === "light" ? "dark" : "light";
17 localStorage.setItem("theme", next);
18 return next;
19 });
20 }, []);
21
22 useEffect(() => {
23 if (!mounted) return;
24 document.documentElement.classList.remove("light", "dark");
25 document.documentElement.classList.add(theme);
26 }, [theme, mounted]);
27
28 const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
29
30 return (
31 <ThemeContext.Provider value={value}>
32 {children}
33 </ThemeContext.Provider>
34 );
35};withTheme Higher-Order Component for easy application wrapping at the provider level.1import React from "react";
2import { ThemeProvider } from "./theme.provider";
3
4export const withTheme = <P extends object>(Component: React.ComponentType<P>) => {
5 return (props: P) => (
6 <ThemeProvider>
7 <Component {...props} />
8 </ThemeProvider>
9 );
10};useTheme hook to switch the visual state.1import { Moon, Sun } from "lucide-react";
2import { Button } from "@/shared/ui";
3import { useTheme } from "../model";
4
5export const ThemeToggle = () => {
6 const { theme, toggleTheme } = useTheme();
7
8 return (
9 <Button onClick={toggleTheme} variant="outline" size="sm">
10 {theme === "light" ? <Moon size={16} /> : <Sun size={16} />}
11 </Button>
12 );
13};