skip to Main Content

I was configuring the context in Tailwind for a Next.js 14 website, and I just wanted to provide a global theme configuration for my project. So, I created the ThemeContext and added the ‘use client’ at the top of the file. However, it still renders on the server side and gives an error.

 ⨯ srccontextsThemeContext.tsx (22:28) @ localStorage
 ⨯ ReferenceError: localStorage is not defined
    at eval (./src/contexts/ThemeContext.tsx:20:29)
    at ThemeProvider (./src/contexts/ThemeContext.tsx:19:78)
  20 | export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  21 |     const [theme, setTheme] = useState<Theme>(() => {
> 22 |         const storedTheme = localStorage?.getItem('theme');
     |                            ^
  23 |         return storedTheme === Theme.DARK ? Theme.DARK : Theme.LIGHT;
  24 |     });
  25 |
 ✓ Comp

I created a mounted state to solve the problem, but I want to understand why this is happening.

'use client';

...

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
    const [theme, setTheme] = useState<Theme>(Theme.LIGHT);
    const [mounted, setMounted] = useState(false);

    useEffect(() => {
        if (mounted) {
            return
        }
        const storedTheme = localStorage?.getItem('theme');
        console.log("storedTheme", storedTheme)
        setTheme(storedTheme === Theme.DARK ? Theme.DARK : Theme.LIGHT);
        setMounted(true)
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    useEffect(() => {
        if (!mounted) {
            return
        }
        if (theme === Theme.DARK) {
            document.documentElement.classList.add('dark');
            localStorage?.setItem('theme', Theme.DARK);
        } else {
            document.documentElement.classList.remove('dark');
            localStorage?.setItem('theme', Theme.LIGHT);
        }
    }, [theme, mounted])

2

Answers


  1. I think you need to update your useEffect in ThemeProvider file. Please check below updated code:-

    useEffect(() => {
        // Check if localStorage is available (i.e., not on the server side)
        if (typeof window !== 'undefined' && !mounted) {
          const storedTheme = localStorage.getItem('theme');
          setTheme(storedTheme === Theme.DARK ? Theme.DARK : Theme.LIGHT);
          setMounted(true);
        }
      }, [mounted]);
    
      useEffect(() => {
        // Apply theme changes to localStorage and document
        if (mounted) {
          if (theme === Theme.DARK) {
            document.documentElement.classList.add('dark');
            localStorage.setItem('theme', Theme.DARK);
          } else {
            document.documentElement.classList.remove('dark');
            localStorage.setItem('theme', Theme.LIGHT);
          }
        }
      }, [theme, mounted]);
    

    Let me know if it works for you or not.

    Login or Signup to reply.
  2. This happens because the component was not yet mounted and you had already called the localstorage object, the localstorage object is specific to the browser and we access it via the window object (window.localstorage), and to access this object you must be sure that the component is already mounted.

    This is where the useEffect comes in, useEffect is called immediately after the first rendering, so this is the best place to call the localstorage object because we are sure at this level that the window object exists.

    There is no need to check if the component is mounted in useEffect, it will always be the case.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search