DevGang
Авторизоваться

Темный режим с Next.js, TypeScript, стилизованными компонентами и набором инструментов Redux

Одна из наиболее распространенных функций, которые мы реализуем, — это темный режим. Однако, реализовав темный режим один раз в проекте, легко забыть, как он был реализован, потому что какое-то время нет возможности его реализовать. Поэтому я решил собрать статью о методе, который использую чаще всего, чтобы каждый мог проверить его в любое время!

Технический стек

Для реализации используются следующие языки, библиотеки и платформы:

  • Next.js
  • TypeScript
  • Стилизованные компоненты
  • Набор инструментов Redux
  • cookies-next

Пошаговая реализация

Конфигурации, константы и типы для цветовых тем

Сначала давайте определим константы и типы, необходимые для управления цветовой темой в const/colorTheme.ts

// const/colorTheme.ts

// Types of available color themes
export const colorThemeNames = [
  'light',
  'dark',
] as const;

// Can't use type ColorThemeName because of circular dependency
export const defaultColorThemeName: typeof colorThemeNames[number] = 'light';

// Cookie key for color theme
export const colorThemeCookieName = 'myAppColorTheme';

и в types/colorTheme.ts

// types/colorTheme.ts

import { colorThemeNames } from '../const/colorTheme';

export type ColorThemeStyle = {
  colors: {
    text: string
    background: string
    componentBackground: string
    border: string
    info: string
    infoBg: string
    danger: string
    dangerBg: string
  },
};

export type ColorThemeName = typeof colorThemeNames[number];

/**
 * Type guard for ColorThemeName
 *
 * @param {unknown} val
 * @return {*}  {val is ColorThemeName}
 */
export const isColorThemeName = (val: unknown): val is ColorThemeName => (
  colorThemeNames.includes(val as ColorThemeName)
);

Также нам нужно изменить тип DefaultTheme в styled.d.ts вот так:

// styled.d.ts

import 'styled-components';
import { ColorThemeStyle } from './types/colorTheme';

declare module 'styled-components' {
  export interface DefaultTheme extends ColorThemeStyle {}
}

Удобно создавать переменные для цветов, которые мы будем использовать.

// const/styles/colors.tsx

export const dryadBark = '#37352f'; // light theme string color
export const white = '#ffffff'; // light theme component color
export const errigalWhite = '#f6f6f9'; // light theme background color
export const gainsboro = '#d9d9d9'; // light theme border color
export const coralRed = '#f93e3d'; // common danger color
export const translucentUnicorn = '#fcecee';
export const softPetals = '#e9f6ef';
export const vegetation = '#48cd90'; // common info color
export const stonewallGrey = '#c3c2c1';
export const astrograniteDebris = '#3b414a'; // dark theme border color
export const aswadBlack = '#141519'; // dark theme background color
export const washedBlack = '#202528'; // dark theme component background color

(Я использовал эту библиотеку для именования этих переменных.)

Затем объявите объекты цветовой темы, тему по умолчанию. config/styles/colorTheme.ts

import { ColorThemeStyle, ColorThemeName } from '../../types/colorTheme';

// colors
import {
  dryadBark,
  white,
  errigalWhite,
  gainsboro,
  coralRed,
  vegetation,
  astrograniteDebris,
  aswadBlack,
  washedBlack,
  softPetals,
  translucentUnicorn,
} from '../../const/styles/colors';

export const defaultColorThemeName: ColorThemeName = 'light';

export const lightTheme: ColorThemeStyle = {
  colors: {
    text: dryadBark,
    background: errigalWhite,
    componentBackground: white,
    border: gainsboro,
    info: vegetation,
    infoBg: softPetals,
    danger: coralRed,
    dangerBg: translucentUnicorn,
  },
};

export const darkTheme: ColorThemeStyle = {
  colors: {
    text: white,
    background: aswadBlack,
    componentBackground: washedBlack,
    border: astrograniteDebris,
    info: vegetation,
    infoBg: softPetals,
    danger: coralRed,
    dangerBg: translucentUnicorn,
  },
};

export const themeNameStyleMap: { [key in ColorThemeName]: ColorThemeStyle } = {
  light: lightTheme,
  dark: darkTheme,
};

export const defaultColorThemeStyle = themeNameStyleMap[defaultColorThemeName];

Настройка набора инструментов Redux

Состояние темного режима должно управляться глобально. Итак, давайте воспользуемся Redux Toolkit для управления глобальным состоянием.

Мы собираемся создать colorThemeSlice, набрать useDispatch, ввести useSelector и настроить store.

stores/slices/colorThemeSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

// "type" is needed. If no "type", circular dependency error arise
// https://stackoverflow.com/questions/63923025/how-to-fix-circular-dependencies-of-slices-with-the-rootstate
import type { RootState } from '../store';
import { ColorThemeName } from '../../types/colorTheme';
import { defaultColorThemeName } from '../../const/colorTheme';

type ColorThemeState = {
  theme: ColorThemeName
};

const initialState: ColorThemeState = {
  theme: defaultColorThemeName,
};

const colorThemeSlice = createSlice({
  name: 'colorTheme',
  initialState,
  reducers: {
    updateColorTheme: (state, action: PayloadAction<ColorThemeName>) => {
      state.theme = action.payload;
    },
  },
});

// selectors
export const selectColorTheme = (state: RootState) => state.colorTheme.theme;
export default colorThemeSlice.reducer;

// actions
export const {
  updateColorTheme,
} = colorThemeSlice.actions;

stores/store.ts

// https://redux-toolkit.js.org/tutorials/typescript

import { configureStore } from '@reduxjs/toolkit';

// reducers
import colorThemeReducer from './slices/colorThemeSlice';

export const store = configureStore({
  reducer: {
    colorTheme: colorThemeReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

stores/hooks.ts

// https://redux-toolkit.js.org/tutorials/typescript

import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { RootState, AppDispatch } from './store';

export const useAppDispatch: () => AppDispatch = useDispatch;

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Служебные функции для обработки файлов cookie цветовой темы

Темный режим должен сохраняться даже при обновлении страницы. Для этого мы будем использовать файлы cookie.

utils/cookie/colorTheme.ts

import { getCookie, setCookie } from 'cookies-next';
import { OptionsType } from 'cookies-next/lib/types';

import { colorThemeCookieName } from '../../const/colorTheme';

import { ColorThemeName, isColorThemeName } from '../../types/colorTheme';

/**
 * Set color theme cookie to persist color theme config
 *
 * @param {ColorThemeName} value
 * @param {OptionsType} [options]
 */
export const setColorThemeCookie = (value: ColorThemeName, options?: OptionsType) => {
  setCookie(colorThemeCookieName, value, options);
};

/**
 *
 *
 * @param {OptionsType} [options]
 * @return {string}  {string}
 */
export const getColorThemeCookie = (options?: OptionsType): string => {
  const colorThemeCookie = getCookie(colorThemeCookieName, options);
  return isColorThemeName(colorThemeCookie) ? colorThemeCookie : '';
};

Реализация пользовательского хука useColorTheme

В реализации нам потребуется переключить цветовые темы или получить текущую цветовую тему. Давайте поместим эту логику в специальный хук, чтобы ее можно было вызывать в каждом компоненте.

hooks/useColorTheme.ts

import { defaultColorThemeName } from '../const/colorTheme';

import { getColorThemeCookie, setColorThemeCookie } from '../utils/cookie/colorTheme';

import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { selectColorTheme, updateColorTheme } from '../stores/slices/colorThemeSlice';

import { themeNameStyleMap } from '../config/styles/colorThemes';

import { ColorThemeName, ColorThemeStyle, isColorThemeName } from '../types/colorTheme';

/**
 * Custom hook for handling color themes
 *
 */
const useColorTheme = () => {
  const dispatch = useAppDispatch();
  const currentColorTheme = useAppSelector(selectColorTheme);

  /**
   * Set color theme cookie and state
   *
   * @param {ColorThemeName} colorThemeName
   */
  const setColorTheme = (colorThemeName: ColorThemeName) => {
    setColorThemeCookie(colorThemeName);
    dispatch(updateColorTheme(colorThemeName));
  };

  /**
   * Initialize color theme cookie and state
   *
   * @return {void}
   */
  const initColorTheme = () => {
    const currentColorThemeCookie = getColorThemeCookie();

    if (!currentColorThemeCookie || !isColorThemeName(currentColorThemeCookie)) {
      setColorTheme(defaultColorThemeName);
      return;
    }

    dispatch(updateColorTheme(currentColorThemeCookie));
  };

  /**
   *
   *
   * @return {*} ColorTheme
   */
  const getCurrentColorThemeState = (): ColorTheme => (
    currentColorThemeState
  );

  /**
   *
   *
   * @return {*}  {ColorThemeStyle}
   */
  const getCurrentColorThemeStyle = (): ColorThemeStyle => (
    themeNameStyleMap[currentColorTheme]
  );

  return {
    setColorTheme,
    initColorTheme,
    getCurrentColorThemeState,
    getCurrentColorThemeStyle,
  };
};

export default useColorTheme;

Настройте RTK и стилизованные компоненты в _app.tsx

На данный момент мы реализовали так много функций и логики, но в нынешнем виде мы пока не можем их использовать. Давайте отредактируем _app.tsx, чтобы убедиться, что Redux Toolkit и стилизованные компоненты доступны для использования.

pages/_app.tsx

import { useEffect, ReactElement, ReactNode } from 'react';

// Next
import { NextPage } from 'next';
import { Router } from 'next/router';
import type { AppProps } from 'next/app';

// Libraries
import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';

import { store } from '../stores/store';

import GlobalStyle from '../components/globalstyles';

import useColorTheme from '../hooks/useColorTheme';

// Layout configuration doc
// https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#with-typescript

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode;
};

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
  router: Router // Error if this property doesn't exist
};

/**
 *
 *
 * @param {AppPropsWithLayout} { Component, pageProps }
 * @return {*} JSX.Element
 */
const WithThemeProviderComponent = ({ Component, pageProps }: AppPropsWithLayout) => {
  const { initColorTheme, getCurrentColorThemeStyle } = useColorTheme();

  useEffect(() => {
    initColorTheme();
  }, []);

  return (
    <ThemeProvider theme={getCurrentColorThemeStyle()}>
      <GlobalStyle />
      <Component {...pageProps} />
    </ThemeProvider>
  );
};

const App = ({ Component, pageProps, router }: AppPropsWithLayout) => {
  const getLayout = Component.getLayout ?? ((page) => page);

  return (
    <Provider store={store}>
      {getLayout(
        <WithThemeProviderComponent
          Component={Component}
          pageProps={pageProps}
          router={router}
        />,
      )}
    </Provider>
  );
};
export default App;

Давайте реализуем переключатель темного режима

С реализацией к этому моменту мы завершили необходимую подготовку к переключению цветовой темы. Теперь давайте воспользуемся useColorTheme для реализации компонента DarkModeToggleSwitch (пока мы пропустим детальную стилизацию).

globalstyles.tsx

...
  body {
    ...
    background-color: ${({ theme }) => theme.colors.background};
    color: ${({ theme }) => theme.colors.text};
    ...
  }

...

components/DarkModeToggleSwitch.tsx

import { useColorTheme } from '../../../hooks/useColorTheme'


/**
 * Dark mode <-> light mode toggle switch
 * Update cookie value and global state
 *
 * @return {*} JSX.Element
 */
const DarkModeToggleSwitch = () => {
  const { setColorTheme, getCurrentColorThemeState } = useColorTheme()

  const currentColorTheme = getCurrentColorThemeState()

  const isDark = currentColorTheme === 'dark'

  const toggleDarkTheme = () => {
    isDark ? setColorTheme('light') : setColorTheme('dark')
  }

  return (
    <>
      ...
      <input type='checkbox' checked={isDark} onChange={toggleDarkTheme} />
      ...
    </>
  )
}

export default DarkModeToggleSwitch

Вот и все. Надеюсь, эта статья была для вас полезна. Помимо этой статьи, существует множество способов реализации переключения цветовой темы. Если у вас есть рекомендуемые методы или статьи, поделитесь ими в комментариях и дайте мне знать!

Источник:

#React #TypeScript #Redux
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу