"localstorage is not defined" nextjs14 "use client" running in server side

53 Views Asked by At

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.

 ⨯ src\contexts\ThemeContext.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

There are 2 best solutions below

0
Bharti Sharma On

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.

0
Djantche Ngamo On

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.