Open dvvolynkin opened 1 year ago
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.
I have 1 page that needs to be with a forced theme
having two global folders looks too much in this case
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
}
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:
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;
}
import { DarkModeWrapper } from "@/components/common/DarkModeWrapper";
export default Page = () => {
return (
<DarkModeWrapper>
<h1>Hello Dark Mode</h1>
</DarkModeWrapper>
);
};
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.
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.
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.
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.
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.
Anybody found a better way to do this yet?
here is what i did (suggestions & improvements are welcome)
//providers/theme-provider.tsx
'use client';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { type ThemeProviderProps } from 'next-themes/dist/types';
import { usePathname } from 'next/navigation';
/*
Most likely your app will have a pre-defined
set of static routes to apply specific themeing.
If this is your case, yay!
*/
const forcedThemeRoutes: Record<string, string> = {
'/app/onboarding': 'light'
//keep adding themes for routes
};
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const pathname = usePathname();
const isForcedThemeRoute = pathname in forcedThemesRoutes;
return (
<NextThemesProvider
{...(isForcedThemeRoute && { forcedTheme: forcedThemesRoutes[pathname] })}
{...props}>
{children}
</NextThemesProvider>
);
}
@rohitDalalStrique that is basically what I ended up doing (as per @pacocoursey guidance above) but it is pretty damn hacky and dynamic paths become problematic. Would be great to find a better solution.
Another solution is to put the ThemeProvider
in 2 separated layouts. @dynjo
For example: given that I would like light/dark theme for the whole website (/
, /blog
, /blog/[slug]
) but not for /curriculum-vitae
which need to always be in light mode.
The structure in the app
directory can be the following:
├── curriculum-vitae
│ ├── layout.tsx
│ └── page.tsx
├── layout.tsx
├── (main)
│ ├── blog
│ │ ├── page.tsx
│ │ └── [slug]
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
With app/(main)/layout.tsx
:
import { ThemeProvider } from "next-themes"
const MainLayout: React.FC = () => {
return <ThemeProvider attribute="class">{children}</ThemeProvider>
}
export default MainLayout
With app/curriculum-vitae/layout.tsx
:
import { ThemeProvider } from "next-themes"
const CurriculumVitaeLayout: React.FC = () => {
return <ThemeProvider attribute="class" forcedTheme="light">{children}</ThemeProvider>
}
export default CurriculumVitaeLayout
This effectively force light theme mode under /curriculum-vitae
paths, but everywhere else in (main)
route group, the theme is dynamic and can be light or dark.
has someone found a better way? I am using the app router but the solutions work but like some say, are a bit hacky
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.