skip to Main Content

I am trying to set the initial value of a Redux Toolkit slice for the dark mode using the localStorage, however, for Next.js the window is not defined on the server-side, causing an error.

Usually, the solution is using the if (typeof window !== "undefined"), but for the initialState it’s not completely correct since a value must be always given even if the window is not defined and you don’t know if that value will be correct. I know lazy initializers exist, however I need an initial value from the beginning of the loading process of the page, since I need to set a darkMode prop to the root HTML element based on that value.

This is my code for the dark mode slice:

"use client";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";

export interface DarkModeState {
    value: boolean;
};

const initialState: DarkModeState = {
    value: window.localStorage.getItem("darkMode")?.toString() === "true"; //! Server error
};

export const darkModeSlice = createSlice({
    name: "darkMode",
    initialState,
    reducers: {
        setDarkMode: (state, action: PayloadAction<boolean>) => {
            state.value = action.payload;
        },
    },
});

export const { setDarkMode } = darkModeSlice.actions;

export default darkModeSlice.reducer;

And this is the root element where I set the darkMode prop:

"use client";
import { useAppSelector } from "@/store/hooks";
import React, { ReactNode, useEffect } from "react";

export default function AppWrapper({ children }: { children: ReactNode }) {
    const darkMode = useAppSelector((state) => state.darkMode.value);
    useEffect(() => {}); // just to make sure it's a client component
    return (
        <div id="app" data-theme-app={darkMode ? "dark" : "light"}>
            {children}
        </div>
    );
};

I already tried using the useEffect hook to match the possibly incorrect dark mode state’s random initial value of the server with the one on the localStorage of the client, however, this leads to an ugly switch of theme during every loading of the page.

Is there a correct way to correctly initialize that dark mode state right from the start of loading process, and so that there are no mismatch problems and hydration issues between server and client?

2

Answers


  1. Chosen as BEST ANSWER

    I was only able to solve this problem using the next-themes package, without Redux Toolkit. This package does everything I need, even avoiding the flickering when loading the page. About the hydration issues, they should expected since the server doesn't know the user's preference beforehand and there is nothing we can do, so, as the package recommends, suppressHydrationWarning should be used where necessary.

    I think there are no workarounds to solve the problem with plain Redux Toolkit other than dynamically loading the whole page with the dynamic() function of next/dynamic and disabling SSR with the option { ssr: false }, which however is not ideal in Next.js.


  2. Try useLayoutEffect instead of useEffect.

    useLayoutEffect is a version of useEffect that fires before the browser repaints the screen.

    useLayoutEffect(() => {
       const isDarkMode = localStorage.getItem("darkMode");
       // And Dispatch ...
    }, []);
    

    https://react.dev/reference/react/useLayoutEffect

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