Pranta
Pranta

Reputation: 3695

nextjs react recoil persist values in local storage: initial page load in wrong state

I have the following code,

const Layout: React.FC<LayoutProps> = ({ children }) => {
    const darkMode = useRecoilValue(darkModeAtom)
    
    console.log('darkMode: ', darkMode)
    return (
        <div className={`max-w-6xl mx-auto my-2 ${darkMode ? 'dark' : ''}`}>
            <Nav />
            {children}
            <style jsx global>{`
                body {
                    background-color: ${darkMode ? '#12232e' : '#eefbfb'};
                }
            `}</style>
        </div>
    )
}

I am using recoil with recoil-persist. So, when the darkMode value is true, the className should include a dark class, right? but it doesn't. I don't know what's wrong here. But it just doesn't work when I refresh for the first time, after that it works fine. I also tried with darkMode === true condition and it still doesn't work. You see the styled jsx, that works fine. That changes with the darkMode value and when I refresh it persists the data. But when I inspect I don't see the dark class in the first div. Also, when I console.log the darkMode value, I see true, but the dark class is not included.

Here's the sandbox link

Maybe it's a silly mistake, But I wasted a lot of time on this. So what am I doing wrong here?

Upvotes: 4

Views: 4879

Answers (3)

Zevi L.
Zevi L.

Reputation: 131

It works for me.

  1. copy these code.
    https://recoiljs.org/docs/guides/atom-effects/#local-storage-persistence
  2. replace localStorage with nookies.
import { parseCookies, setCookie, destroyCookie } from "nookies";

const cookies = parseCookies();
const localStorageEffect =
  (key) =>
  ({ setSelf, onSet }) => {
    const savedValue = cookies[key];
    if (savedValue != null) {
      setSelf(JSON.parse(savedValue));
    }

    onSet((newValue, _, isReset) => {
      isReset
        ? destroyCookie(null, key)
        : setCookie(null, key, JSON.stringify(newValue));
    });
  };
  1. fixed Hydration issue.
    https://stackoverflow.com/a/72318597/5451474

my demo code

Upvotes: 0

cnotethegr8
cnotethegr8

Reputation: 7510

Extending on @aleksxor solution, you can perform the useEffect once as follows.

First create an atom to handle the SSR completed state and a convenience function to set it.

import { atom, useSetRecoilState } from "recoil"

const ssrCompletedState = atom({
  key: "SsrCompleted",
  default: false,
})

export const useSsrComplectedState = () => {
  const setSsrCompleted = useSetRecoilState(ssrCompletedState)
  return () => setSsrCompleted(true)
}

Then in your code add the hook. Make sure it's an inner component to the Recoil provider.

const setSsrCompleted = useSsrComplectedState()
useEffect(setSsrCompleted, [setSsrCompleted])

Now create an atom effect to replace the recoil-persist persistAtom.

import { AtomEffect } from "recoil"
import { recoilPersist } from "recoil-persist"

const { persistAtom } = recoilPersist()

export const persistAtomEffect = <T>(param: Parameters<AtomEffect<T>>[0]) => {
  param.getPromise(ssrCompletedState).then(() => persistAtom(param))
}

Now use this new function in your atom.

export const darkModeAtom = atom({
  key: "darkMode",
  default: false,
  effects_UNSTABLE: [persistAtomEffect]
})

Upvotes: 5

aleksxor
aleksxor

Reputation: 8340

The problem is that during SSR (server side rendering) there is no localStorage/Storage object available. So the resulted html coming from the server always has darkMode set to false. That's why you can see in cosole mismatched markup errors on hydration step.

I'd assume using some state that will always be false on the initial render (during hydration step) to match SSR'ed html but later will use actual darkMode value. Something like:

// themeStates.ts
import * as React from "react";
import { atom, useRecoilState } from "recoil";
import { recoilPersist } from "recoil-persist";

const { persistAtom } = recoilPersist();

export const darkModeAtom = atom<boolean>({
  key: "darkMode",
  default: false,
  effects_UNSTABLE: [persistAtom]
});

export function useDarkMode() {
  const [isInitial, setIsInitial] = React.useState(true);
  const [darkModeStored, setDarkModeStored] = useRecoilState(darkModeAtom);

  React.useEffect(() => {
    setIsInitial(false);
  }, []);

  return [
    isInitial === true ? false : darkModeStored,
    setDarkModeStored
  ] as const;
}

And inside components use it like that:

// Layout.tsx
  const [darkMode] = useDarkMode();
// Nav.tsx
  const [darkMode, setDarkMode] = useDarkMode();

codesandbox link

Upvotes: 10

Related Questions