You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js中如何添加ThemeProvider实现明暗模式切换?

在Next.js中正确配置styled-components的ThemeProvider实现明暗模式切换

问题背景

在Next.js应用中尝试添加styled-components的ThemeProvider以实现明暗模式切换时,遇到两个冲突的错误:

  1. 未添加"use client"指令时,抛出错误:Error: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it
  2. 添加"use client"后,又触发编译错误:ReactServerComponentsError: You are attempting to export "metadata" from a component marked with "use client", which is disallowed

核心原因

Next.js的根布局(RootLayout)默认是Server Component,既不能直接使用客户端专属的API(比如createContext,而styled-components的ThemeProvider依赖它),又必须保留metadata导出(Server Component专属特性)。直接在根布局中混用两种逻辑就会导致冲突。

解决方案:拆分客户端与服务端逻辑

把ThemeProvider相关的客户端逻辑抽离成独立的Client Component,让RootLayout保持为Server Component,各司其职。

步骤1:创建独立的ThemeProvider客户端组件

新建components/ThemeProviderClient.tsx,专门处理主题状态、上下文和样式:

'use client';

import { useState, useEffect, createContext } from 'react';
import { ThemeProvider as StyledThemeProvider } from 'styled-components';
import { lightTheme, darkTheme, GlobalStyles } from '@/styles/ThemeConfig';

// 创建主题切换上下文,供子组件调用切换方法
export const ThemeToggleContext = createContext<{ toggleTheme: () => void }>({
  toggleTheme: () => {},
});

export default function ThemeProviderClient({ children }: { children: React.ReactNode }) {
  // 初始化主题:优先取localStorage缓存,其次匹配系统深色模式偏好
  const [theme, setTheme] = useState(() => {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      return savedTheme === 'dark' ? darkTheme : lightTheme;
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? darkTheme : lightTheme;
  });

  // 切换主题方法
  const toggleTheme = () => {
    setTheme(prev => prev === darkTheme ? lightTheme : darkTheme);
  };

  // 主题变更时同步到localStorage,实现持久化
  useEffect(() => {
    localStorage.setItem('theme', theme === darkTheme ? 'dark' : 'light');
  }, [theme]);

  return (
    <StyledThemeProvider theme={theme}>
      <GlobalStyles />
      <ThemeToggleContext.Provider value={{ toggleTheme }}>
        {children}
      </ThemeToggleContext.Provider>
    </StyledThemeProvider>
  );
}

步骤2:修改RootLayout.tsx

移除原有的ThemeProvider导入,改用新的客户端组件,保持RootLayout为Server Component并保留metadata:

import "./globals.css";
import "@/styles/nav.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ApolloProvider } from "@apollo/client";
import createApolloClient from "./apollo";
import Header from "./header";
import ThemeProviderClient from "@/components/ThemeProviderClient";

const inter = Inter({ subsets: ["latin"] });
const client = createApolloClient();

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ApolloProvider client={client}>
          <ThemeProviderClient>
            <Header />
            {children}
          </ThemeProviderClient>
        </ApolloProvider>
      </body>
    </html>
  );
}

步骤3:在Header中添加主题切换按钮

修改Header.tsx,通过上下文调用切换方法:

'use client';

import { useContext } from 'react';
import { ThemeToggleContext } from '@/components/ThemeProviderClient';

export default function Header() {
  const { toggleTheme } = useContext(ThemeToggleContext);

  return (
    <header style={{ padding: '1rem', display: 'flex', justifyContent: 'flex-end' }}>
      <button onClick={toggleTheme} style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}>
        切换明暗模式
      </button>
    </header>
  );
}

说明

  • 拆分后,RootLayout作为Server Component负责页面元数据、字体、ApolloProvider等服务端/全局配置;
  • ThemeProviderClient作为Client Component处理主题状态、上下文和样式切换,完全符合Next.js的组件模型规则;
  • 加入了localStorage持久化和系统偏好检测,用户刷新页面后主题不会丢失,首次访问时自动匹配系统设置。

内容的提问来源于stack exchange,提问作者teilai teilai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 11:05:10