From 0e06afcd4dfd88d712db917057309787a1b51c64 Mon Sep 17 00:00:00 2001 From: Farasat Ali Date: Sun, 23 Jul 2023 23:20:03 +0500 Subject: [PATCH] Fixed Issue Related to Hydration Error For AppDir --- README.md | 32 +++++++++++++++++++++++++----- src/cookie-helper.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++ src/index.tsx | 13 ++++++++++++ 3 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 src/cookie-helper.ts diff --git a/README.md b/README.md index ee13fb7..eae96b1 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 {children} +export function Providers({ children, ...props }: ThemeProviderProps) { + return {children} } ``` @@ -94,18 +96,38 @@ Then add the `` component to your layout _inside_ of the ``. 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 ( - + - {children} + + {children} + ) } ``` -> **Note!** If you do not add [suppressHydrationWarning](https://reactjs.org/docs/dom-elements.html#suppresshydrationwarning:~:text=It%20only%20works%20one%20level%20deep) to your `` 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. +> **Note!** If you do not add [suppressHydrationWarning](https://reactjs.org/docs/dom-elements.html#suppresshydrationwarning:~:text=It%20only%20works%20one%20level%20deep) to your `` 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. + +> suppressHydrationWarning is not required any more! Also using theme from useTheme hook will also not cause hydration error. ### HTML & CSS diff --git a/src/cookie-helper.ts b/src/cookie-helper.ts new file mode 100644 index 0000000..b192b8c --- /dev/null +++ b/src/cookie-helper.ts @@ -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(); diff --git a/src/index.tsx b/src/index.tsx index f342956..116acbd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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'] @@ -87,6 +88,7 @@ const Theme: React.FC = ({ // Save to storage try { localStorage.setItem(storageKey, theme) + cookieManager.setCookie(storageKey, theme || defaultTheme); } catch (e) { // Unsupported } @@ -106,6 +108,15 @@ const Theme: React.FC = ({ [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) @@ -147,6 +158,8 @@ const Theme: React.FC = ({ systemTheme: (enableSystem ? resolvedTheme : undefined) as 'light' | 'dark' | undefined }), [theme, setTheme, forcedTheme, resolvedTheme, enableSystem, themes]); + if (!theme) setTheme(defaultTheme); + return (