Как стать автором
Обновить

Создание темы Next.js Typescript emotion

Уровень сложностиСредний

Данная статья содержит ответы на вопросы:
1) Как внедрить emotion в проект на next.js?
2) Как сделать кастомную тему для каждого пользователя?
3) Как сохранить стили в localStorage.

Создание проекта и установка библиотек

Для начала создадим проект при помощи команды: npx create-next-app@latest . После введения команды, отвечая на вопрос: "Would you like to use TypeScript?", выбираем вариант: "Yes". На остальные вопросы выбираем ответ по своему усмотрению.

После создания проекта, нам надо установить библиотеку: @emotion/react. Данная библитека нам нужна для написания стилей в виде объектов. Это нам нужно для того, чтоб любой пользователь мог изменить тему на сайте абсолютно как угодно и сохранить. Код для установки библиотеки: npm i @emotion/react

Создание темы

Теперь нам нужно создать файл index.ts и colors.ts в папке: src/assets/theme или /assets/theme, если при создании проекта уже не было папки src.

// colors.ts
// Light
export const colorLight = "#fff";

// Dark
export const colorDark = "#181818";
// index.ts
import { colorLight, colorDark } from "./colors";

export const theme = {
  light: {
    background: colorLight,
    fontColor: colorDark,
  },
  dark: {
    background: colorDark,
    fontColor: colorLight,
  },
};
export const defaultTheme = theme.light;

export type CurrentThemeType = typeof theme.light;

Создание Context

Вы можете использовать любой другой удобный store. Я же в качестве примера, буду использовать Context из React. Store нам нужен для того, чтоб из любого реакт компонента мы могли узнать об актуальных стилях для темы.

// store/index.tsx

// Импортируем переменную: theme, которую создали ранее
import { defaultTheme } from "@/assets/theme";
import type { CurrentThemeType } from "@/assets/theme";

import { createContext, ReactNode, useMemo, useState } from "react";

export interface IContext {
  theme: CurrentThemeType;
  setTheme: (theme: CurrentThemeType) => void;
}
export const Context = createContext<IContext | undefined>(undefined);

export interface ContextProviderProps {
  children: ReactNode;
}
export const ContextProvider = ({ children }: ContextProviderProps) => {
  const [theme, setTheme] = useState(defaultTheme);

  const handleChangeTheme = (value: CurrentThemeType) => {
    setTheme(value);
  };

  // Кладем данные в useMemo, чтоб избежать создания нового объекта при каждом 
  // новом рендере, если объект не изменился
  const contextValue = useMemo(
    () => ({
      theme,
      setTheme: handleChangeTheme,
    }),
    [theme]
  );

  return <Context.Provider value={contextValue}>{children}</Context.Provider>;
};

Создание компонента Layout

// layouts/MainLayout.tsx

// После 13 версии next.js стало обязательным добавление: "use client" при разработке
// клиентских компонентов.
"use client";

import { ReactNode, useCallback, useContext, useEffect } from "react";
import { css } from "@emotion/react";
import { theme as themeData } from "@/assets/theme";
import { Context, ContextProvider } from "@/store";

export interface MainLayoutProps {
  children: ReactNode;
}

export function MainLayout({ children }: MainLayoutProps) {
  const context = useContext(Context);

  const handleChangeTheme = useCallback(
    (isDark: boolean) => {
      context?.setTheme(isDark ? themeData.dark : themeData.light);
    },
    [context]
  );

  useEffect(() => {
    // Проверяем: "Темная ли тема у пользователя на утройстве?".
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

    mediaQuery.addEventListener("change", (e) => handleChangeTheme(e.matches));

    handleChangeTheme(mediaQuery.matches);

    // Очищаем прослушивание события
    return () => {
      mediaQuery.removeEventListener("change", (e) =>
        handleChangeTheme(e.matches)
      );
    };
  }, [handleChangeTheme]);

  return (
    <ContextProvider>
      <body
        // Данный пропс нам доступен только с библиотекой @emotion/react
        css={css`
          background: ${context?.theme.background};
          color: ${context?.theme.fontColor};
        `}
      >
        {children}
      </body>
    </ContextProvider>
  );
}
// app/layout.tsx

// После 13 версии next.js стало обязательным добавление: "use client" при разработке
// клиентских компонентов.
"use client";

import { ReactNode } from "react";

import { ContextProvider } from "@/store";
import { MainLayout } from "@/layouts/MainLayout";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <ContextProvider>
      <html lang="ru">
        <MainLayout>{children}</MainLayout>
      </html>
    </ContextProvider>
  );
}

Конфиги

После установки проекта у вас уже будут конфигурационные файлы в корне приложения. Для корректной работы с библиотекой: @emotion/react, нам надо внести некоторые изменения в tsconfig.json.

// tsconfig.json

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    // Самое главное - добавить данное поле. Остальное можете не менять в данном файле.
    "jsxImportSource": "@emotion/react",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.