Данная статья содержит ответы на вопросы:
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"]
}