4

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?

DeBraid
  • 8,751
  • 5
  • 32
  • 43
Pranta
  • 2,928
  • 7
  • 24
  • 37

3 Answers3

10

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

aleksxor
  • 7,535
  • 1
  • 22
  • 27
  • Thanks, that totally worked. I just have a question though, do I always have to make a custom hook to solve this kind of problem? – Pranta Jun 24 '21 at 10:07
  • 2
    I believe most of the libraries working with SSR are accounting for that behavior from the start. I'm surprised the `recoil-persist` doesn't. Though maybe I'm missing something obvious. – aleksxor Jun 24 '21 at 10:29
5

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]
})
cnotethegr8
  • 7,342
  • 8
  • 68
  • 104
0

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

Zevi L.
  • 131
  • 2
  • 4