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

Next.js 13 appDir support #152

Closed
transitive-bullshit opened this issue Nov 13, 2022 · 64 comments
Closed

Next.js 13 appDir support #152

transitive-bullshit opened this issue Nov 13, 2022 · 64 comments

Comments

@transitive-bullshit
Copy link

transitive-bullshit commented Nov 13, 2022

Opening an issue to track support for Next.js 13's appDir and server components.

Currently, it's possible to use next-themes within appDir with the following pattern:

// app/layout.tsx
import * as React from 'react'

import { Providers } from './providers'
import './globals.css'

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <head>
      </head>

      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  )
}
// app/providers.tsx
'use client'

import { ThemeProvider } from 'next-themes'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider>
      {children}
    </ThemeProvider>
  )
}

This works for the most part, including accessing and changing the theme via useTheme.

However, during next dev, we get the following console error hinting at hydration problems:

Warning: Extra attributes from the server: data-theme,style
    at html
    at ReactDevOverlay (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:53:9)
    at HotReload (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:19:11)
    at Router (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/app-router.js:74:11)
    at ErrorBoundaryHandler (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:28:9)
    at ErrorBoundary (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:40:11)
    at AppRouter
    at ServerRoot (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/app-index.js:113:11)
    at RSCComponent
    at Root (webpack-internal:///./node_modules/.pnpm/next@13.0.3_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/app-index.js:130:11)

And during production (hosted on Vercel), the following hydration errors seem to randomly appear regardless of how much I pair down the example so long as I'm using next-themes in this way:

Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at zh (796-3c8db907cc96f9a4.js:9:55756)
    at ...

#145 looks to be a cookies-based approach to solving this issue, but the PR looks stalled and I'm not sure if it's a working solution or even the best approach. So I wanted to open up an issue here to track suggestions & progress.

Thanks!

@transitive-bullshit transitive-bullshit changed the title Next.js 13 support appDir support Next.js 13 appDir support Nov 13, 2022
@litewarp
Copy link

Can't speak to all the issues brought up, but the hydration errors occur because of the way that next/react streams the html. If you replace the <script> tag in the ThemeScript component with the <NextScript /> component, it should hydrate correctly.

@WITS
Copy link
Contributor

WITS commented Nov 16, 2022

#145 was working with an earlier build of Next.js, but it definitely looks like using <NextScript /> can clean that pr up quite a bit now. I'm gonna try to update it this week

@transitive-bullshit
Copy link
Author

Thanks @WITS 🙏

@leerob would you be able to provide any guidance on the best solution here (or forward to someone who might)?

@WITS
Copy link
Contributor

WITS commented Nov 17, 2022

That pr should be updated to work with Next.js 13.0.3 now. I'm still looking into making further improvements, but you can test the changes through the npm package @wits/next-themes prior to the pr being merged. (Or try running npm i "next-themes@npm:@wits/next-themes" if you want to keep the same imports in your code.)

I'd like to provide some more context on why the current approach uses cookies. Unfortunately, trying to get this working in the app directory without ensuring that the server is rendering the same <html> attributes results in errors. So I reached out to @pacocoursey as well as some members of the Next.js team before starting this work, and they agreed that a cookie-based approach would be the right move.

next/script is a great call! It wasn't available for the app directory when I first started writing these changes, but it looks like using it with stategy="beforeInteractive" works here. However, doing so inside of a client component (like the existing ThemeProvider still causes it to run too late and the page will flicker un-themed content. Also, by default, that script relies on window.matchMedia to determine the system theme of the user. That isn't available on the server, so without cookies we still get errors due to a mismatch between client and server rendering during fast refreshes.

Update: Please make sure you use the <ServerThemeProvider> (as outlined here) if you're trying this out. If you want to have theme switcher UI, you'll need both the <ServerThemeProvider> in the layout and a <ThemeProvider> in an interactive ("use client") component.

Update 2: The fork above is no longer being maintained! Please see this comment for instructions on how to use the stable version of next-themes with appDir.

@transitive-bullshit
Copy link
Author

Appreciate the in-depth breakdown @WITS.

I just tried @wits/next-themes on my project, and while I can verify it's taking effect, I'm not seeing any changes with regards to the error messages. On next dev, the same hydration error occurs w.r.t. extra html attributes, and on production, the same hydration errors appear sporadically (same as previously).

This could be a problem with my app, of course, but the hydration errors during next dev give me pause.

@WITS
Copy link
Contributor

WITS commented Nov 17, 2022

Would you be able to share your updated app/layout.tsx (or a simplified version of it) so I can reproduce the issue locally?

@transitive-bullshit
Copy link
Author

transitive-bullshit commented Nov 18, 2022

Definitely not a minimal repro, but my source is here: https://github.com/transitive-bullshit/next-movie/blob/main/app/layout.tsx. See RootLayoutProviders.

I currently have my usage of next-themes commented out in the providers and in a few places in the code because no matter what I tried, I would get sporadic hydration errors on prod that would cause the entire client-side JS to stop working — resulting in a dead app for users. It should be easy to re-enable once next-themes is patched.

@WITS
Copy link
Contributor

WITS commented Nov 18, 2022

Ah, I should have clarified when I explained the approach above that it requires a new component. Can you try wrapping the <html> tag in your layout.tsx file with the <ServerThemeProvider> from @wits/next-themes and see if that works?

(There's a section in the updated README with an example.)

@transitive-bullshit
Copy link
Author

transitive-bullshit commented Nov 19, 2022

Ahhh good catch.

Okay, so after doing that and re-enabling my usage of next-themes@npm:@wits/themes, I'm seeing the following.

Dev

During next dev the first few times it loads is fine, but as soon as I toggle dark mode client-side and hard refresh, I start seeing mismatched hydration warnings in the dev console:

CleanShot 2022-11-18 at 17 59 54@2x

CleanShot 2022-11-18 at 18 00 04@2x

Prod

It works well hydration-wise 💯 I haven't been able to reproduce any hydration errors when deployed as a preview build to Vercel: https://next-movie-hrwhglaz6-saasify.vercel.app/

However, I'm now seeing different, consistent 500 errors from Vercel using this branch with next-themes enabled that doesn't repro on main:

Error: invariant: invalid Cache-Control duration provided: 0 < 1

CleanShot 2022-11-18 at 18 10 06@2x

Example: https://next-movie-hrwhglaz6-saasify.vercel.app/titles/424

@WITS I'm going to hold off on re-enabling dark mode toggle in prod for now. I'm not too worried about the next dev errors, so long as it works on prod, though we should probably get them fixed before merging #145. If you want to test locally, check out https://github.com/transitive-bullshit/next-movie/tree/feature/next-themes-beta and set DATABASE_URL to a Postgres instance. You may also need to do npx prisma db push or npx prisma generate.

I think this cache-control issue invariant issue is likely a bug with next@canary, though it's strange that it only repros on this branch where the only changes involve enabling next-themes.

@transitive-bullshit
Copy link
Author

I created a few Next.js issues to track the errors I'm running into. Not sure what else I can do to help.

@willin
Copy link

willin commented Nov 25, 2022

image

[0] event - compiled client and server successfully in 837 ms (350 modules)
[0] wait  - compiling /_error (client and server)...
[0] error - (sc_server)/node_modules/.pnpm/next-themes@0.2.1_7iuvftg57tblwyxclfkwku5xo4/node_modules/next-themes/dist/index.js (1:223) @ eval
[0] error - TypeError: e.createContext is not a function
[0]     at eval (webpack-internal:///(sc_server)/./node_modules/.pnpm/next-themes@0.2.1_7iuvftg57tblwyxclfkwku5xo4/node_modules/next-themes/dist/index.js:11:92)
[0]     at Object.(sc_server)/./node_modules/.pnpm/next-themes@0.2.1_7iuvftg57tblwyxclfkwku5xo4/node_modules/next-themes/dist/index.js (/Users/v0/Sites/alpha/.next/server/app/[locale]/page.js:345:1)
[0]     at __webpack_require__ (/Users/v0/Sites/alpha/.next/server/webpack-runtime.js:33:42)
[0]     at eval (webpack-internal:///(sc_server)/./app/[locale]/themes.tsx:11:69)
[0]     at Object.(sc_server)/./app/[locale]/themes.tsx (/Users/v0/Sites/alpha/.next/server/app/[locale]/page.js:313:1)
[0]     at __webpack_require__ (/Users/v0/Sites/alpha/.next/server/webpack-runtime.js:33:42)
[0]     at eval (webpack-internal:///(sc_server)/./app/[locale]/page.tsx:6:65)
[0]     at Object.(sc_server)/./app/[locale]/page.tsx (/Users/v0/Sites/alpha/.next/server/app/[locale]/page.js:292:1)
[0]     at Function.__webpack_require__ (/Users/v0/Sites/alpha/.next/server/webpack-runtime.js:33:42)
[0]     at processTicksAndRejections (node:internal/process/task_queues:96:5) {
[0]   type: 'TypeError',
[0]   page: '/[locale]'
[0] }

@willin
Copy link

willin commented Nov 25, 2022

That pr should be updated to work with Next.js 13.0.3 now. I'm still looking into making further improvements, but you can test the changes through the npm package @wits/next-themes prior to the pr being merged. (Or try running npm i "next-themes@npm:@wits/next-themes" if you want to keep the same imports in your code.)

I'd like to provide some more context on why the current approach uses cookies. Unfortunately, trying to get this working in the app directory without ensuring that the server is rendering the same <html> attributes results in errors. So I reached out to @pacocoursey as well as some members of the Next.js team before starting this work, and they agreed that a cookie-based approach would be the right move.

next/script is a great call! It wasn't available for the app directory when I first started writing these changes, but it looks like using it with stategy="beforeInteractive" works here. However, doing so inside of a client component (like the existing ThemeProvider still causes it to run too late and the page will flicker un-themed content. Also, by default, that script relies on window.matchMedia to determine the system theme of the user. That isn't available on the server, so without cookies we still get errors due to a mismatch between client and server rendering during fast refreshes.

image

[0] event - compiled client and server successfully in 655 ms (199 modules)
[0] wait  - compiling /middleware (client and server)...
[0] event - compiled successfully in 51 ms (67 modules)
[0] wait  - compiling /[locale]/page (client and server)...
[0] event - compiled client and server successfully in 440 ms (362 modules)
[0] TypeError: Cannot read properties of undefined (reading '_context')
[0]     at Object.useContext (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react/cjs/react.shared-subset.development.js:1418:29)
[0]     at exports.useTheme (webpack-internal:///(sc_server)/./node_modules/.pnpm/@wits+next-themes@0.2.12_7iuvftg57tblwyxclfkwku5xo4/node_modules/@wits/next-themes/dist/index.js:215:28)
[0]     at ChangeTheme (webpack-internal:///(sc_server)/./app/[locale]/themes.tsx:147:84)
[0]     at attemptResolveElement (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1207:42)
[0]     at resolveModelToJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1660:53)
[0]     at Object.toJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1121:40)
[0]     at stringify (<anonymous>)
[0]     at processModelChunk (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:172:36)
[0]     at retryTask (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1868:50)
[0]     at performWork (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1906:33)
[0] TypeError: Cannot read properties of undefined (reading '_context')
[0]     at Object.useContext (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react/cjs/react.shared-subset.development.js:1418:29)
[0]     at exports.useTheme (webpack-internal:///(sc_server)/./node_modules/.pnpm/@wits+next-themes@0.2.12_7iuvftg57tblwyxclfkwku5xo4/node_modules/@wits/next-themes/dist/index.js:215:28)
[0]     at ChangeTheme (webpack-internal:///(sc_server)/./app/[locale]/themes.tsx:147:84)
[0]     at attemptResolveElement (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1207:42)
[0]     at resolveModelToJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1660:53)
[0]     at Object.toJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1121:40)
[0]     at stringify (<anonymous>)
[0]     at processModelChunk (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:172:36)
[0]     at retryTask (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1868:50)
[0]     at performWork (webpack-internal:///(sc_server)/./node_modules/.pnpm/next@13.0.5_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1906:33) {
[0]   digest: '3476920131'
[0] }

@dawidseipold
Copy link

@WITS Hi, I am using the @wits/next-themes ServerThemeProvider, but I still get the same hydration error. Not sure what the problem might be. Here's the code snippet (it's a fresh project).

const RootLayout = ({ children }: IProps) => {
  return (
    <ServerThemeProvider>
      <html lang="en">
        <head />

        <Providers>
          <body className="bg-red-500 dark:bg-blue-500">{children}</body>
        </Providers>
      </html>
    </ServerThemeProvider>
  );
};

After adding attribute='class' props to the provider the hydration still error persists, but is displayed after this error Cannot read properties of null (reading 'textContent'). Any help would be appreciated :)

@WITS
Copy link
Contributor

WITS commented Nov 30, 2022

I published a new version of @wits/next-themes (0.2.13) today that includes some fixes.

@transitive-bullshit Good catch! The "e" value was due to a bug in my pr, and should be fixed now. However, I can still reproduce the hydration issues whenever I explicitly set the theme to something other than system. I think this might be an issue with Next caching the first value it receives for the cookie after the server starts. I'm going to investigate further and reach out to them if that's the case.

@willin Is your project using <ServerThemeProvider> in the layout?

@dawidseipold Are you using the same properties on your <ServerThemeProvider> as your <ThemeProvider>? (If you're setting attribute="class" on one, it needs to be on both.)

@dawidseipold
Copy link

@WITS Yep! The hydration error is doubled if I do that. Having only <ServerThemeProvider> gives me one error.

@WITS
Copy link
Contributor

WITS commented Dec 1, 2022

Okay, just published 0.2.14 which should fix more hydration errors. (Maybe all of them? 🤞)

@willin
Copy link

willin commented Dec 3, 2022

Okay, just published 0.2.14 which should fix more hydration errors. (Maybe all of them? 🤞)

nope,

RootLayout:

      <ServerThemeProvider attribute='class' themes={themes.map((x) => x.id)}>
        <html lang={locale}>
          <head />
          <body>
            <div
              id='background'
              className={clsx({
                dark: true //darkThemes.map((x) => x.toLowerCase()).includes(theme)
              })}></div>
            {/* <Header /> */}
            <div className='pt-20' style={{ minHeight: 'calc(100vh - 75px)' }}>
              {children}
            </div>
            <Bootstrap />
          </body>
        </html>
      </ServerThemeProvider>

Component:
image

image

current theme none, set theme not work.

@willin
Copy link

willin commented Dec 3, 2022

if a nest a ThemeProvider inside ServerThemeProvider:

image

@willin
Copy link

willin commented Dec 3, 2022

is there a working example using class?

@litewarp
Copy link

litewarp commented Dec 4, 2022

Hey @WITS, I can get the page to render using both <ServerThemeProvider /> and a client <ThemeProvider/>, but I can't get setTheme to work. I'll try and reproduce it for you. In the meantime, I noticed that when using both providers, the script gets injected twice. Not sure if this would be the cause of some issues.

image

@dawidseipold
Copy link

@willin same problem when I nest. It also shows in the console that the classes on html element are different.

@balthazar
Copy link

Hey @WITS, I haven't managed to get your 0.2.14 to properly function (with the ServerProvider and ThemeProvider), but it might be because I'm on Next 13.0.6?

Warning: Cannot render a sync or defer <script> outside the main document without knowing its order. Try adding async="" or moving it into the root tag.
Warning: Expected server HTML to contain a matching <script> in .

I'd also like to get your thoughts on the cookie-based approach you elaborated above would play out with edge caching. If the page is requested once by a user with a "dark" cookie, we will likely get a flash of wrong theme if another requests the same cached page with a different cookie correct? Using localStorage was a way around that issue, but I guess Server Components might not leave us much of a choice. I don't think there's a way to use the Vary cache header on one particular cookie.

@joshuajeschek
Copy link

@anjanah07, why did you add the useEffect? In my case, it works without it, and if I wait for the component to render, I get a flash of light mode. Could you elaborate on your solution?

@statusunknown418
Copy link

statusunknown418 commented May 6, 2023

still happening on next 13.4.1 ThemeProvider seems to be the cause, has someone found a fix?

@tfsomrat
Copy link

tfsomrat commented May 9, 2023

✅SOLUTIONS:

This is how I am using this right now:

Providers.tsx

"use client";
import { ThemeProvider } from "next-themes";
import { ReactNode } from "react";

const Providers = ({ children }: { children: ReactNode }) => {
  return (
    <ThemeProvider attribute="class" >
      {children}
    </ThemeProvider>
  );
};

export default Providers;

layout.tsx

<html suppressHydrationWarning={true}>
      <body>
        <Providers>
          <Header />
          <main>{children}</main>
          <Footer />
        </Providers>
      </body>
    </html>

@ledunguit
Copy link

@tfsomrat This is not a solution, it only turns off the warning but the bug still remains.

@statusunknown418
Copy link

@tfsomrat This is not a solution, it only turns off the warning but the bug still remains.

Exactly, I think there should be a way to really fix this

@ahmaadaziz
Copy link

So far what solution is being used for NextJs 13 app dir? I have read through the issues and there are a few solutions and propositions to fix this problem, but which is the best one so far which also enables SSR or SSG?

@exceptionptr
Copy link

exceptionptr commented Jun 10, 2023

What I do to resolve this whole issue:

Set cookie in middleware.

middleware.ts

export function middleware(request: NextRequest) {
  const theme = request.cookies.get('theme')?.value ?? 'dark';

  const response = NextResponse.next();

  response.cookies.set('theme', theme, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: new Date(2147483647 * 1000),
  });

  return response;
}

Getting the theme even in server side components.

actions.client.ts

export const getTheme = () => {
  return cookies().get('theme')?.value ?? 'light';
};

Theme switcher using the experimental server actions.

actions.server.ts

'use server';

import { cookies } from 'next/headers';

export const setTheme = async (theme: string) => {
  cookies().set('theme', theme, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: new Date(2147483647 * 1000),
  });
};

This way I don't even have to resolve the hydration issue for next-images using "empty" images, until the client can resolve the theme. I just wrap next-images into a component:

ThemeImage.tsx

import Image from 'next/image';
import { ComponentPropsWithoutRef } from 'react';

import { getTheme } from '@/helpers/actions.client';

export const ThemeImage = ({
  srcLight,
  srcDark,
  ...props
}: {
  srcLight: string;
  srcDark: string;
} & Omit<ComponentPropsWithoutRef<typeof Image>, 'src'>) => {
  const theme = getTheme();

  return <Image src={theme === 'light' ? srcLight : srcDark} {...props} />;
};

For my project it is enough to set the class of the html tag in the root layout based on the cookies value.

layout.tsx

import { getTheme } from '@/helpers/actions.client';

import './style.css';

const Layout = async ({ children }: { children: React.ReactNode }) => {
  const theme = getTheme();

  return (
    <html lang='en' className={theme}>
      <body>{children}</body>
    </html>
  );
};

export default Layout;

@resherra
Copy link

Hi, do you have any ideas how to enable themes (dark/light) only for particular pages? Otherwise, it should be always light theme. I was trying to do set the forcedTheme, but it causes the page flickering, as it works on client side. Any help is appreciated, thank you!

'use client';

import { usePathname } from 'next/navigation';
import { ThemeProvider as PreferredProvider } from 'next-themes';

// eslint-disable-next-line react/prop-types
const ThemeProvider = ({ children }) => {
  const pathname = usePathname();
  const isDocPage = pathname.startsWith('/docs'); 

  return (
    <PreferredProvider attribute="class" forcedTheme={isDocPage ? null : 'light'}>
      {children}
    </PreferredProvider>
  );
};
export default ThemeProvider;

Hey @vannyle , any updates about this!

@vannyle
Copy link

vannyle commented Jul 18, 2023

lazych

Hi, unfortunately, there is no update since.

@estubmo
Copy link

estubmo commented Jul 30, 2023

What I do to resolve this whole issue:

Set cookie in middleware.

It works, but as a side effect, it causes a fetch each time you toggle themes. It also does not support system themes, which I'm not entirely sure how it could.

@WITS
Copy link
Contributor

WITS commented Aug 3, 2023

I see there's still a lot of discussion here about how to best support next-themes in the app directory. I know that there's a lot of comments in this thread, so I wanted to summarize some of the findings from near the beginning and some issues I think I see people running into:

  • It's impossible to detect the system theme from the server, so you must inject the inline script
  • If you use cookies() in the app directory, any pages relying on that code will not support SSG (Static Site Generation)
  • If you wait until a client component is mounted (e.g. using useEffect), you're susceptible to FART (Flash of inAccurate coloR Theme)
  • If you wrap the <ThemeProvider> in a client component, it works completely fine, but React will give a hydration warning because it modifies the <html> element immediately on the client
  • You can not render the <Providers> component (or any component that's rendering visual elements) outside of the <body>. You must make sure it's inside of the <body>
  • suppressHydrationWarning on the <html> element is safe in this case, because it's only silencing warnings about the attributes of the <html> element not matching. You will still see warnings for any other hydration issues

I recognize that suppressHydrationWarning doesn't seem like an ideal solution, and I don't think that property should be used without ruling out every other option. However, it's the only option I've tried thus far that doesn't break a fundamental part of either next-themes or Next.js, and it's been used in production apps for several months now without issue. For an example of how to use this solution, please view the updated README

@michaelangeloio
Copy link

Feel free to check out my article on how I made Tailwind theming work with the Next 13 app DIr.
https://michaelangelo.io/blog/darkmode-rsc

@manovotny
Copy link

Feel free to check out my article on how I made Tailwind theming work with the Next 13 app DIr.
https://michaelangelo.io/blog/darkmode-rsc

@michaelangelo-io while this will work, it's important to note that using cookies in the root layout will make you're *entire app dynamic. ⚠️

From the Next.js Docs (source):

Good to know: cookies() is a Dynamic Function whose returned values cannot be known ahead of time. Using it in a layout or page will opt a route into dynamic rendering at request time.

@michaelangeloio
Copy link

Feel free to check out my article on how I made Tailwind theming work with the Next 13 app DIr.
https://michaelangelo.io/blog/darkmode-rsc

@michaelangelo-io while this will work, it's important to note that using cookies in the root layout will make you're *entire app dynamic. ⚠️

From the Next.js Docs (source):

Good to know: cookies() is a Dynamic Function whose returned values cannot be known ahead of time. Using it in a layout or page will opt a route into dynamic rendering at request time.

Good callout. Updated to note your comment and also an interesting solution for the static side of things: https://michaelangelo.io/blog/darkmode-rsc#important-nextjs-caveat

@useEffects
Copy link

Why is this still open?

@mario1in
Copy link

Still meet this issue. Even if wait until the page is mounted.

next: 14.0.4
next-themes: 0.2.1

@nasirify
Copy link

nasirify commented Nov 30, 2023

Hi!
I've just found the solution.
each step is essential:

  1. move all your providers to a component called something like Providers
  2. lazy load the component with dunamic and ssr:false in the layout

there are some points that some of you missed in your code:

  • you shoud use 'use client' directive and the top of your Providers components
  • not all of your providers need to go into the dynamic loaded Providers component except those which you want to disable ssr

look at my codes:
layout.tsx:

import 'public/styles/globals.css'
// notice that how I am loading my Provider conponent:
import dynamic from 'next/dynamic'
const Providers = dynamic(() => import('./providers'), { ssr: false })



export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html >
      <body >
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

notice the ssr:false and lazy load with dynamic
Providers.tsx

'use client'
import { ThemeProvider } from '@/components/shadcn/theme-provider'

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider
      attribute='class'
      defaultTheme='light'
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </ThemeProvider>
  )
}

it works fine in nextjs 14

hope it will help you <3

@michaelbrusegard
Copy link

Hi! I've just found the solution. each step is essential:

  1. move all your providers to a component called something like Providers
  2. lazy load the component with dunamic and ssr:false in the layout

there are some points that some of you missed in your code:

  • you shoud use 'use client' directive and the top of your Providers components
  • not all of your providers need to go into the dynamic loaded Providers component except those which you want to disable ssr

look at my codes: layout.tsx:

import 'public/styles/globals.css'
// notice that how I am loading my Provider conponent:
import dynamic from 'next/dynamic'
const Providers = dynamic(() => import('./providers'), { ssr: false })



export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html >
      <body >
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

notice the ssr:false and lazy load with dynamic Providers.tsx

'use client'
import { ThemeProvider } from '@/components/shadcn/theme-provider'

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider
      attribute='class'
      defaultTheme='light'
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </ThemeProvider>
  )
}

it works fine in nextjs 14

hope it will help you <3

This worked for me in Next.js 14, but for some reason I had to remove { ssr: false } or set it to true to avoid a white flash when refreshing with a dark theme

@michaelbrusegard
Copy link

I forgot to say that I still have to use suppressHydrationWarning on the html element

@Moriarty47
Copy link

What I do to resolve this whole issue:

Set cookie in middleware.

When you are on a slow network, the page will wait for a long time for the server response before switching themes.
Imagine that after clicking the switcher, you wait for about 10 seconds, or even longer, the page finally brightens or darkens.

@krAtOsnana
Copy link

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en"
style={{ colorScheme: 'dark' }}
className={(fontSans.variable, 'dark')} // <--
>
<body
className={cn("min-h-screen font-sans antialiased bg-dark-300", fontSans.variable)}
>

{children}



);
}
this solved my issue #317

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

No branches or pull requests