Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to force theme (app router) #226

Open
dvvolynkin opened this issue Oct 26, 2023 · 9 comments
Open

How to force theme (app router) #226

dvvolynkin opened this issue Oct 26, 2023 · 9 comments

Comments

@dvvolynkin
Copy link

How can I properly force a specific theme for a page using the app router?

I've looked at the example in the README, but it only cover the case for pages.

@linkb15
Copy link

linkb15 commented Oct 27, 2023

In my case, I have 2 theme providers where one is forced and one is not forced. And in App Router, we can use the grouping folder (group1) and (group2).

Put the non forced in (group1) folder and forced one inside the layout of (group2) folder. In this case, the layouts will affects only the pages inside each own groups.

@dvvolynkin
Copy link
Author

I have 1 page that needs to be with a forced theme

having two global folders looks too much in this case

@dvvolynkin
Copy link
Author

I made a provider like this
But it doesn't solve the problem completely, for a second when loading the original theme appears

"use client"

import * as React from "react";
import { ThemeProvider as NextJSThemesProvider } from "next-themes";
import { ThemeProviderProps as NextJSThemesProviderProps } from "next-themes/dist/types";

interface ForcedThemeContextProps {
  forcedTheme: string | null;
  setForcedTheme: React.Dispatch<React.SetStateAction<string | null>>;
}

const ForcedThemeContext = React.createContext<ForcedThemeContextProps | undefined>(undefined);

export function useForcedThemeControl(): ForcedThemeContextProps {
  const context = React.useContext(ForcedThemeContext);
  if (!context) {
    throw new Error("useForcedThemeControl must be used within a ForcedThemeContextProvider");
  }
  return context;
}

interface ForcedThemeContextProviderProps {
  children: React.ReactNode;
}

export function ForcedThemeContextProvider({ children }: ForcedThemeContextProviderProps): JSX.Element {
  const [forcedTheme, setForcedTheme] = React.useState<string | null>(null);
  return (
    <ForcedThemeContext.Provider value={{ forcedTheme, setForcedTheme }}>
      {children}
    </ForcedThemeContext.Provider>
  );
}

interface ThemeSetterProps {
  children: React.ReactNode;
}

function DarkTheme({ children }: ThemeSetterProps): JSX.Element {
  const { setForcedTheme } = useForcedThemeControl();
  React.useEffect(() => {
    setForcedTheme("dark");
  }, []);
  return <>{children}</>;
}

function LightTheme({ children }: ThemeSetterProps): JSX.Element {
  const { setForcedTheme } = useForcedThemeControl();
  React.useEffect(() => {
    setForcedTheme("light");
  }, []);
  return <>{children}</>;
}

interface CombinedThemeProviderProps extends NextJSThemesProviderProps {
  children: React.ReactNode;
}

const CombinedThemeProvider = ({ children, ...props }: CombinedThemeProviderProps): JSX.Element => {
  const { forcedTheme } = useForcedThemeControl();
  return (
    <NextJSThemesProvider {...props} forcedTheme={forcedTheme || undefined}>
      {children}
    </NextJSThemesProvider>
  );
};

function ThemeProvider({ children, ...props }: CombinedThemeProviderProps): JSX.Element {
  return (
    <ForcedThemeContextProvider>
      <CombinedThemeProvider {...props}>
        {children}
      </CombinedThemeProvider>
    </ForcedThemeContextProvider>
  );
}

export {
  ThemeProvider,
  LightTheme,
  DarkTheme
}

@rafaelquintanilha
Copy link

I was facing the same problem. Creating route segments would be enough, but I wanted to avoid this as I also had only one page with forced theme.

The solution I came up with was the following:

  1. Create a DarkModeWrapper client component:
"use client";

import { useTheme } from "next-themes";
import { useEffect } from "react";

export function DarkModeWrapper({ children }: { children: React.ReactNode }) {
  const { setTheme } = useTheme();

  useEffect(() => {
    setTheme("dark");
    return () => {
      setTheme("light");
    };
  }, []);

  return children;
}
  1. Wrap the desired page with it:
import { DarkModeWrapper } from "@/components/common/DarkModeWrapper";

export default Page = () => {
  return (
    <DarkModeWrapper>
      <h1>Hello Dark Mode</h1>
    </DarkModeWrapper>
  );
};

@dvvolynkin
Copy link
Author

dvvolynkin commented Mar 6, 2024

Isn't setTheme setting theme globally?

In this case, your local wrapper is affecting the whole website

    setTheme("dark");
    return () => {
      setTheme("light");
    };
  }, []);

This code will make the dark theme on one page but will set the light theme for other pages even if the dark theme is selected there.

@rafaelquintanilha
Copy link

Isn't setTheme setting theme globally?

In this case, your local wrapper is affecting the whole website

    setTheme("dark");
    return () => {
      setTheme("light");
    };
  }, []);

This code will make the dark theme on one page but will set the light theme for other pages even if the dark theme is selected there.

That was my case, but I suppose you can set resolvedTheme to a useRef and then just pass it when unmounting the component.

@dvvolynkin
Copy link
Author

dvvolynkin commented Mar 6, 2024

That was my case, but I suppose you can set resolvedTheme to a useRef and then just pass it when unmounting the component.

Open two pages, one with the light theme and one with your implementation of forced dark.
Opening the dark theme page will change the theme of the first one.

Also selecting any theme on the main website theme selector will change the dark theme page.

@rafaelquintanilha
Copy link

That was my case, but I suppose you can set resolvedTheme to a useRef and then just pass it when unmounting the component.

Open two pages, one with the light theme and one with your implementation of forced dark. Opening the dark theme page will change the theme of the first one.

Also selecting any theme on the main website theme selector will change the dark theme page.

You're right. Sorry, I just covered my very narrow use-case.

What I ended up doing, however, is what is described here. That works fine with Tailwind because I can set the dark specifier on the inner component. It works correctly in multiple tabs as well. However, if it's important for you, this won't change resolvedTheme, as pointed out.

@pacocoursey
Copy link
Owner

pacocoursey commented Mar 13, 2024

I solved this in a hacky way: using usePathname() in a client component, then determining whether the theme should be forced based on some regex matching for specific paths. Then, I passed the forced theme to the ThemeProvider. Pseudo code example here:

"use client"
import { usePathname } from 'next/navigation'
import { ThemeProvider } from 'next-themes'

export const Providers = (props) => {
	const pathname = usePathname();
	const forcedThemeFromPathname = pathname === "/dark-only" ? "dark" : undefined;

	return (
		<ThemeProvider forcedTheme={forcedThemeFromPathname}>
			{props.children}
		</ThemeProvider>
	)
}

I'd like to find a better solution but so far I've got nothing. We need a way to pass information up from a page.tsx (server component) file to the root layout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants