Руководство по реализации переключения тем (light/dark) с использованием React Context, HOC и localStorage.
Преимущество: Изоляция логики темы от бизнес-компонентов. Автоматическое сохранение выбора пользователя и отсутствие 'мигания' при загрузке.
Находится в src/app/providers/theme/. Отвечает за глобальную инициализацию.
Контекст, хуки и UI-компонент выносятся в shared/, так как они переиспользуемы.
light, dark) и структуру контекста для управления состоянием.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 с проверкой на наличие провайдера.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 и манипулирует классами на html.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 для удобного оборачивания приложения на уровне провайдеров.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 для смены визуального стейта.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};