使用Nextjs和Tailwind实现深色主题的教程

发表时间: 2023-02-12 08:26

1.简介

随着 nextjs 成为开发 React 应用程序的黄金标准,我想我应该简要解释一下如何使用 nextjs 和 tailwind 创建一个漂亮的深色主题。 我们将构建以下内容

主题切换器

主题上下文提供者

用法示例

到最后你将能够

先决条件:

配置了 tailwind 的正在运行的 nextjs 应用程序。

以下依赖项:js-cookie@^3.0.1 & @types/js-cookie@^3.0.2 坚持使用主题为 windcss@3.0.5 设置页面样式

2.主题上下文提供者

在我们开始之前。 Tailwind 通过 CSS 子选择器实现他们的深色主题。 基本上,如果您的 HTML 元素具有 class="dark",它们将自动应用所有 dark:some-tailwind-class 样式。 这就是为什么围绕它的所有功能都将涉及在切换主题时添加/删除 class="dark"。

我们将使用 createContext 来跟踪主题并在需要时使用它。 在这种情况下。

但是为什么我们要使用 React 的上下文呢? 它会导致很多重新渲染和..东西!

.. 我要说的是:

“nu-uh!react 的上下文是一个依赖注入工具,如果我们不破坏它的预期用途,我们就不会造成任何伤害!”

对于只想复制和粘贴所有内容的每个人,只需滚动到底部即可完成工作。

2.1. 创建上下文。

这是相当简单的,所以我不会深入探讨任何细节。

const initialState = false; // we start the first-time visitors up on the light themeexport const ThemeContext = createContext({  isDarkTheme: initialState, // pass in the inital state  toggleThemeHandler: () => {  }, // define a function to toggle the theme});

2.2. 主题上下文提供者

上下文提供者必须处理以下两种情况。

新用户:默认为浅色主题设置浅色主题cookie在HTML元素上设置class="light"

返回用户:阅读 cookieCall setIsDarkTheme 并在 HTML 元素上使用适当的 valueSet class="light" 或 class="dark"(从技术上讲,我们不需要 light 类,但我喜欢将其保留在那里)

在用户想要时更改主题

我们将需要两个函数。 一个用于初始化主题并处理案例 1 和 2。还有一个用于实际切换主题的函数。

2.2.1. 初始化程序

ThemeContextProvider 跟踪我们当前在其自己的 useState 调用中启用的主题。 您可以完全依赖 cookie,但我发现这有点麻烦。 因此,我们有 const [isDarkTheme, setIsDarkTheme] = useState(initialState); 在最顶端。 这样我们也可以在用户决定更改主题时调用 initialThemeHandler,而不必将 initialThemeHandler 函数与 setTheme 函数分开。

  const [isDarkTheme, setIsDarkTheme] = useState(initialState);const initialThemeHandler = useCallback((): void => {  // Get current cookie theme.  const themeCookie = Cookies.get("theme");  // js-cookie returns `undefined` if there's no cookie by that name  setIsDarkTheme(themeCookie === "dark");  // Here we start handling the case when we have a returning user (because we found a cookie)  if (themeCookie) {    // Just to be super-duper sure we're adding the right classes, remove what ever the other theme is    document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light");    // The cookie will have the value of `dark` or `light`    // Therefore we can just set it to the value of the cookie    document.querySelector("html")?.classList.add(themeCookie);  } else {    // Oooo!! A new user!    // I set the cookie expiration to 30 days, but that's optional    const date = new Date();    const expires = new Date(date.setMonth(date.getMonth() + 1));    // Set the default light cookie    Cookies.set("theme", "light", {      secure: true,      expires: expires,    });  }  // we're going to call this callback everytime the `isDarkTheme` property changes}, [isDarkTheme]);// an dalso on the initial renderuseEffect(() => initialThemeHandler(), [initialThemeHandler]);

2.2.2. 主题切换功能

这是上下文将通过(跟我说)依赖注入提供给其他组件的功能。

  function toggleThemeHandler(): void {  // get the current theme cookie. We know it exists since this will 100% of the time run after the `initialThemeHandler` function  const themeCookie = Cookies.get("theme");  // What ever theme we previously had, set it to the opposite.  // Remember this is a boolean!  setIsDarkTheme((ps) => !ps);  // Create a new cookie expiration date  const date = new Date();  const expires = new Date(date.setMonth(date.getMonth() + 1));  // Set the cookie to the opposit of what ever it's currently holding  Cookies.set("theme", themeCookie !== "dark" ? "dark" : "light", {    secure: true,    expires: expires,  });  // add the appropriate class to the `HTML element  toggleDarkClassToHTMLElement();}

所有这个函数所做的就是删除深色或浅色,然后将深色或浅色添加到 HTML 元素。我选择这个元素是因为它是最上面的一个,我遇到了 nextjs 和更改 bodyclasses 的一些问题。

  function toggleDarkClassToHTMLElement(): void {  document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light");  document.querySelector("html")?.classList.add(!isDarkTheme ? "dark" : "light");}

最后我们像这样返回 ThemeContext.Provider

  return (  <ThemeContext.Provider    value={      {        isDarkTheme, // remember, this is a boolean        toggleThemeHandler // handler to toggle the theme      }}>    {props.children} // all other components will be child components of this one  </ThemeContext.Provider>);

接下来我们需要初始化主题。 那应该处理以下情况

  const initialThemeHandler = useCallback((): void => {  // Get current cookie theme.  const themeCookie = Cookies.get("theme");  // js-cookie returns `undefined` if there's no cookie by that name  setIsDarkTheme(themeCookie === "dark");  // Here we start handling the case when we have a returning user (because we found a cookie)  if (themeCookie) {    // Just to be super-duper sure we're adding the right classes, remove what ever the other theme is    document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light");    // The cookie will have the value of `dark` or `light`    // Therefore we can just set it to the value of the cookie    document.querySelector("html")?.classList.add(themeCookie);  } else {    // Oooo!! A new user!    // I set the cookie expiration to 30 days, but that's optional    const date = new Date();    const expires = new Date(date.setMonth(date.getMonth() + 1));    // Set the default light cookie    Cookies.set("theme", "light", {      secure: true,      expires: expires,    });  }  // we're going to call this callback everytime the `isDarkTheme` property changes}, [isDarkTheme]);// an dalso on the initial renderuseEffect(() => initialThemeHandler(), [initialThemeHandler]);

3.应用主题

因为我们希望我们所有的组件都能够访问当前主题并切换它,所以我们将在提供程序中包装我们的 entirenextjs 应用程序。 为此,我们创建一个 _app.tsx 文件并将所有组件包装在提供程序中,如下所示

export default function Root({Component, pageProps}: AppProps) {  return (    <ThemeContextProvider>      <Component {...pageProps} />    </ThemeContextProvider>  );}

现在,我们将使用老式按钮来切换主题。

interface IThemeTogglerContext{    isDarkTheme: boolean;    toggleThemeHandler: () => void;}export function ThemeToggleButton(){  // get the `toggleThemeHandler` via *dependency injection*  const { toggleThemeHandler }: IThemeTogglerContext = useContext(ThemeContext);  // toggle the theme onClick  function toggle(){    toggleThemeHandler()  }  // super fancy button  return (  <button     onClick    class={classNames(         // styles which won't be affected by the theme        "font-bold py-2 px-4 rounded-full",        // light theme styles        "bg-blue-500 hover:bg-blue-700 text-white",        // dark theme styles        "dark:bg-blue-100 dark:hover:bg-blue-200 dark:text-red-50",     )}>    Toggle Theme  </button>)}

然后,您可以将此按钮放在任何您想要的位置,它会更新主题。 正如一开始提到的,tailwind 使用 CSS 选择器应用深色主题,因此您想要在深色主题中使用的任何样式,只需在选择器前加上一个 dark: 前缀,因为它是 d

4. Body样式化,添加主题切换转场

因为我想在更改主题时看到一些小的过渡,所以我还创建了一个 _document.tsx 文件并添加了一些顺风类,这使得主题切换成为一种愉快的体验。 这是完成的工作

import Document, { Head, Html, Main, NextScript } from "next/document";export default class _Document extends Document {  render() {    return (      <Html>        <Head>          <title>dle.dev</title>         </Head>        <body className="bg-neutral-50 dark:bg-neutral-900 transition-colors overflow-x-hidden ">          <Main />          <NextScript />        </body>      </Html>    );  }}

是的,我知道我的 Head / Title 配置不是最佳实践。 在这里查看 Vercel 对这个主题的看法。

5. 整个片段

给你,这就是全部! 如果您想尝试一下,这里是完整的 ThemeContext 组件:

import type { ReactElement, ReactNode } from "react";import { createContext, useCallback, useEffect, useState } from "react";import Cookies from "js-cookie";const initialState = false;export const ThemeContext = createContext({  isDarkTheme: initialState,  toggleThemeHandler: () => {},});interface ThemePropsInterface {  children: ReactNode;}export function ThemeContextProvider(props: ThemePropsInterface): ReactElement {  const [isDarkTheme, setIsDarkTheme] = useState(initialState);  const initialThemeHandler = useCallback((): void => {    const themeCookie = Cookies.get("theme");    setIsDarkTheme(themeCookie === "dark");    if (themeCookie) {      document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light");      document.querySelector("html")?.classList.add(themeCookie);    } else {      const date = new Date();      const expires = new Date(date.setMonth(date.getMonth() + 1));      Cookies.set("theme", "light", {        secure: true,        expires: expires,      });    }  }, [isDarkTheme]);  useEffect(() => initialThemeHandler(), [initialThemeHandler]);  function toggleThemeHandler(): void {    const themeCookie = Cookies.get("theme");    setIsDarkTheme((ps) => !ps);    const date = new Date();    const expires = new Date(date.setMonth(date.getMonth() + 1));    Cookies.set("theme", themeCookie !== "dark" ? "dark" : "light", {      secure: true,      expires: expires,    });    toggleDarkClassToBody();  }  function toggleDarkClassToBody(): void {    document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light");    document.querySelector("html")?.classList.add(!isDarkTheme ? "dark" : "light");  }  return (    <ThemeContext.Provider value={{ isDarkTheme, toggleThemeHandler }}>      {props.children}    </ThemeContext.Provider>  );}