DEV Community

Cover image for Change Themes in Next.js 14 without Wrapping Children in Context .
keshav jha
keshav jha

Posted on • Updated on

Change Themes in Next.js 14 without Wrapping Children in Context .

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 4: 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;

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (8)

Collapse
 
yuens1002 profile image
sunny yuen

thank you so much for this, works fine with nextJS 14.2.11. i did want to point out in app/layout.tsx, AppThemeProvider needs to accept a children prop. I ended up wrapping the theme switching component inside AppThemeProvider in app/layout.tsx.

Collapse
 
mekkj98 profile image
keshav jha • Edited

@yuens1002 Thanks for your kind word.

if your are using tailwind css, you can skip passing children to to AppThemeProvider from layout.tsx. Because while initial hydration you don't need to know about the theme because you are already passing theme class into html tag's class attribute which will select dark or light theme for user without flicker.

Also, if you want to access the theme for switching or showing on the ui which theme is selected. You can just wrap that part of ui inside AppThemeProvider in client components. Whole purpose here is to not wrap application inside context in layout.tsx and tailwindcss.

Now if it is completely necessary for you to wrap your children into AppThemeProvider then don't use this method instead use template.js/jsx/tsx file and make that a child component and inside that Wrap AppThemeProvider around your children.

Thank you.

Collapse
 
yuens1002 profile image
sunny yuen • Edited

looking at this...

// Application theme provider

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

and this in your app/layout.tsx where AppThemeProvider is being called without a children

<body className={${MonaSans_Font.className} flex flex-col h-full w-full overflow-hidden`}>
{children}

    <AppThemeProvider 
       attribute="class" 
       defaultTheme={theme} 
       enableSystem 
    />
  </body>`
Enter fullscreen mode Exit fullscreen mode

this will cause a type error as it is being called without the required children prop as it is typed as a requirement in the AppTHemeProvider function signature.

Thread Thread
 
mekkj98 profile image
keshav jha

Ok, sorry for that. I think it shouldn't however if i m wrong you can always pass null to avoid those error. In react null is valid children.
Thanks

Collapse
 
franklin030601 profile image
Franklin Martinez

I like this! Thanks 🙌

Collapse
 
terradestroyer profile image
Terra Destroyer

can you tell more about tabs components in the last code? I tried to use select component. I got an error with theme.tsx as shown in first code :

 You're importing a component that needs useEffect. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mekkj98 profile image
keshav jha

I might not be able to help without full context or code demo.

This may help: if you have forgot to add "use client"; in first line of step 3 file, that's why it might be throwing you error. The main purpose of this approach was to use client component only for the components which is toggling the theme that way you can keep your app server rendered while rendering theme switch in client side dynamically.

Collapse
 
terradestroyer profile image
Terra Destroyer

uh.. which step 3? (there are 2 step 3s ... I guess)
if you mean when integrating ThemeProvider in app/layout.tsx, is it possible to add "use client" in RootLayout?
And If you mean in the last step I already done that.