Implement Theme Provider to Change Themes without Wrapping Children in Context in Next.js/React.Js

Implement Theme Provider to Change Themes without Wrapping Children in Context in Next.js/React.Js

Introduction

In Web applications, theming plays a crucial role in providing a personalised and visually appealing user experience. One common approach is to use a ThemeProvider Context to manage themes across the application. There is no problem with application's which is completely client side. This blog is specific to developer who is looking for a solution where there application is combination of client and server components and theming depends on color-schema or className.

With the release of Next.js 14+, wrapping children in a Context will make children a client component. In this blog post, we'll explore how to implement a ThemeProvider which will help us to avoid wrapping complete application into a Context provider.

Implementing Theme Provider in Next.js 14+

To implement the Theme Provider without wrapping children in Context, we'll follow these steps:

  1. Install Dependencies: Ensure you have the necessary dependencies installed, including next-themes for theme management and cookies-next for cookies management.

  2. Create Theme Provider Component: Develop a custom ThemeProvider component that handles theme switching and cookie management.

  3. Integrate Theme Provider: Integrate the ThemeProvider component into your application layout to make sure client theming is synced with server on hydration.

  4. Use Theme Switcher : Integrate the ThemeProvider component into your application to switch theme.

Let's dive deeper into each step.

Step 1: Install Dependencies

First, make sure you have next-themes installed in your Next.js project. If not, you can install it using npm or yarn:

npm install next-themes cookies-next
# or
yarn add next-themes cookies-next

Step 2: Create Theme Provider Component

In your project, create a custom ThemeProvider component that leverages next-themes for theme management. This component will handle theme switching and cookie management for server-side rendering.

// components/context/theme.tsx
import { setCookie } from "cookies-next";
import { ThemeProvider, useTheme } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
import { useEffect } from "react";

// Application theme provider
function AppThemeProvider({ children, ...props }: ThemeProviderProps) {
  return (
    <ThemeProvider enableColorScheme {...props}>
      <AppThemeProviderHelper />
      {children}
    </ThemeProvider>
  );
}

// Helper component to set theme in cookie
function AppThemeProviderHelper() {
  const { theme } = useTheme();

  useEffect(() => {
    setCookie("__theme__", theme, {
      expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
      path: "/",
    });
  }, [theme]);

  return null;
}

export default AppThemeProvider;

Step 3: Integrate Theme Provider

Now, integrate the ThemeProvider component into your application layout in your app/layout.tsx file or any child layout as per requirement:

import { MonaSans_Font } from "@/assets/fonts/MonaSans";
import dynamic from "next/dynamic";

const AppThemeProvider = dynamic(() => import("@/components/context/theme"), {
  ssr: false,
});

export default async function AppLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const theme = cookies().get("__theme__")?.value || "system";

  return (
    <html
      className={theme}
      lang="en"
      style={theme !== "system" ? { colorScheme: theme } : {}}
    >
      <body className={`${MonaSans_Font.className} flex flex-col h-full w-full overflow-hidden`}>
        {children}

        <AppThemeProvider 
           attribute="class" 
           defaultTheme={theme} 
           enableSystem 
        />
      </body>
    </html>
  );
}

Step 3: Integrate Theme Provider in Theme Switching

A Tab's based switcher can be created as below to switch theme Using AppThemeProvider.

"use client";

import AppThemeProvider from "@/components/context/theme";
import { Tabs, TabsList, TabsTrigger } from "ui/components/tabs";
import { useTheme } from "next-themes";

function Tab() {
  const { setTheme, theme } = useTheme();
  return (
    <Tabs className="w-full" onValueChange={setTheme} value={theme}>
      <TabsList className="grid w-full grid-cols-3">
        <TabsTrigger value="light">Light</TabsTrigger>
        <TabsTrigger value="dark">Dark</TabsTrigger>
        <TabsTrigger value="system">System</TabsTrigger>
      </TabsList>
    </Tabs>
  );
}

function ThemeTabs() {
  return (
    <AppThemeProvider attribute="class" defaultTheme="system" enableSystem>
      <Tab />
    </AppThemeProvider>
  );
}

export default ThemeTabs;

Conclusion

Implementing a Theme Provider in Next.js 14+ without wrapping children in Context offers a cleaner and more efficient way to manage themes in your application without making complete application a client component.