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

Fixed Issue Related to Hydration Error For AppDir #195

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ An abstraction for themes in your Next.js app.
- ✅ Force pages to specific themes
- ✅ Class or data attribute selector
- ✅ `useTheme` hook
- ✅ No hydration error in AppDir

Check out the [Live Example](https://next-themes-example.vercel.app/) to try it for yourself.

Expand Down Expand Up @@ -80,9 +81,10 @@ Adding dark mode support still only takes a few lines of code. Start by creating
'use client'

import { ThemeProvider } from 'next-themes'
import type { ThemeProviderProps } from "next-themes/dist/types";

export function Providers({ children }) {
return <ThemeProvider>{children}</ThemeProvider>
export function Providers({ children, ...props }: ThemeProviderProps) {
return <ThemeProvider {...props}>{children}</ThemeProvider>
}
```

Expand All @@ -94,18 +96,38 @@ Then add the `<Providers>` component to your layout _inside_ of the `<body>`.
import { Providers } from './providers'

export default function Layout({ children }) {
// just add it as it is
const storageKey = "theme";
const cookieStore = cookies();
let myTheme = cookieStore.get(storageKey)?.value;
const attributeType = cookieStore.get("attributeType")?.value;
myTheme = myTheme || "light";

return (
<html suppressHydrationWarning>
<html
lang="en"
data-theme={myTheme}
style={{ colorScheme: myTheme }}
className={`${attributeType || ""}`}
>
<head />
<body>
<Providers>{children}</Providers>
<Providers
defaultTheme={myTheme}
storageKey={storageKey}
// ...some more props
>
{children}
</Providers>
</body>
</html>
)
}
```

> **Note!** If you do not add [suppressHydrationWarning](https://reactjs.org/docs/dom-elements.html#suppresshydrationwarning:~:text=It%20only%20works%20one%20level%20deep) to your `<html>` you will get warnings because `next-themes` updates that element. This property only applies one level deep, so it won't block hydration warnings on other elements.
> <del>**Note!** If you do not add [suppressHydrationWarning](https://reactjs.org/docs/dom-elements.html#suppresshydrationwarning:~:text=It%20only%20works%20one%20level%20deep) to your `<html>` you will get warnings because `next-themes` updates that element. This property only applies one level deep, so it won't block hydration warnings on other elements.</del>

> suppressHydrationWarning is not required any more! Also using theme from useTheme hook will also not cause hydration error.

### HTML & CSS

Expand Down
47 changes: 47 additions & 0 deletions src/cookie-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
interface CookieOptions {
path?: string;
expires?: Date | number;
domain?: string;
secure?: boolean;
}

class CookieManager {
getCookie(name: string): string | undefined {
const cookieStr = document.cookie;
const cookies = this.parseCookieString(cookieStr);
return cookies[name];
}

setCookie(name: string, value: string, options?: CookieOptions): void {
let cookieStr = `${name}=${encodeURIComponent(value)}`;

if (options) {
if (options.path) cookieStr += `; path=${options.path}`;
if (options.expires) {
const expirationDate =
options.expires instanceof Date
? options.expires.toUTCString()
: new Date(Date.now() + options.expires * 1000).toUTCString();
cookieStr += `; expires=${expirationDate}`;
}
if (options.domain) cookieStr += `; domain=${options.domain}`;
if (options.secure) cookieStr += `; secure`;
}

document.cookie = cookieStr;
}

private parseCookieString(cookieStr: string): { [key: string]: string } {
const cookieMap: { [key: string]: string } = {};
const cookiePairs = cookieStr.split(";");

cookiePairs.forEach((pair) => {
const [key, value] = pair.trim().split("=");
cookieMap[key] = decodeURIComponent(value);
});

return cookieMap;
}
}

export const cookieManager = new CookieManager();
13 changes: 13 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
useMemo,
memo
} from 'react'
import {cookieManager} from "./cookie-helper"
import type { UseThemeProps, ThemeProviderProps } from './types'

const colorSchemes = ['light', 'dark']
Expand Down Expand Up @@ -87,6 +88,7 @@ const Theme: React.FC<ThemeProviderProps> = ({
// Save to storage
try {
localStorage.setItem(storageKey, theme)
cookieManager.setCookie(storageKey, theme || defaultTheme);
} catch (e) {
// Unsupported
}
Expand All @@ -106,6 +108,15 @@ const Theme: React.FC<ThemeProviderProps> = ({
[theme, forcedTheme]
)

useEffect(() => {
cookieManager.setCookie("attribute", attribute);
}, [attribute])

useEffect(() => {
if (!cookieManager.getCookie(storageKey))
cookieManager.setCookie(storageKey, theme || defaultTheme);
}, [])

// Always listen to System preference
useEffect(() => {
const media = window.matchMedia(MEDIA)
Expand Down Expand Up @@ -147,6 +158,8 @@ const Theme: React.FC<ThemeProviderProps> = ({
systemTheme: (enableSystem ? resolvedTheme : undefined) as 'light' | 'dark' | undefined
}), [theme, setTheme, forcedTheme, resolvedTheme, enableSystem, themes]);

if (!theme) setTheme(defaultTheme);

return (
<ThemeContext.Provider
value={providerValue}
Expand Down