Ryan
Ryan

Reputation: 24035

How to solve react-hydration-error in Next.js when using `useLocalStorage` and `useDebounce`

When I try to use https://usehooks-ts.com/react-hook/use-local-storage in Next.js in the following way, I get

Unhandled Runtime Error Error: Text content does not match server-rendered HTML.

See more info here: https://nextjs.org/docs/messages/react-hydration-error

  const [toleranceH, setToleranceH] = useLocalStorage<number>('toleranceH', 3);
  const [toleranceS, setToleranceS] = useLocalStorage<number>('toleranceS', 3);
  const [toleranceL, setToleranceL] = useLocalStorage<number>('toleranceL', 3);

  const [results, setResults] = useState<MegaColor[]>([]);

  const debouncedToleranceH = useDebounce<number>(toleranceH, 200);
  const debouncedToleranceS = useDebounce<number>(toleranceS, 200);
  const debouncedToleranceL = useDebounce<number>(toleranceL, 200);

  useEffect(() => {
    const targetColorDetailsObject = getColorDetailsObject(targetColor);
    const degreeTolerance = (360 / 100) * debouncedToleranceH;
    const [hueMin, hueMax] = getHueTolerance(targetColorDetailsObject.hue(), degreeTolerance);
    const filteredColors = getFilteredColors(targetColorDetailsObject, loadedMegaColors, hueMin, hueMax, debouncedToleranceS, debouncedToleranceL);
    setResults(filteredColors);
    return () => {
      // console.log('cleanup');
    };
  }, [targetColor, loadedMegaColors, debouncedToleranceH, debouncedToleranceS, debouncedToleranceL]); 

From that help page, I still can't figure out what to adjust so that I can use both useLocalStorage and useDebounce.

I found https://stackoverflow.com/a/73411103/470749 but don't want to forcefully set a localStorage value (it should only be set by the user).

Upvotes: 6

Views: 6078

Answers (1)

htmn
htmn

Reputation: 1675

I'd suggest checking out this excellent post on rehydration by Josh W Comeau.

Since Next.js pre-renders every page by default you need to ensure that the component in which you are calling window.localstorage is only rendered on the client.

A simple solution is to:

  1. Keep a hasMounted state
const [hasMounted, setHasMounted] = useState(false);
  1. Toggle it inside a useEffect
useEffect(() => {
  // This will only be called once the component is mounted inside the browser
  setHasMounted(true);
}, []);
  1. Add a check so that Next.js won't complain about prerendering stuff on the server that won't match the stuff that gets rendered on the client
if (!hasMounted) {
  return null;
}
  1. Ensure that the client-side stuff comes after the check

To make it more reusable you could use one of these two methods which essentially do the same:

ClientOnly Component

function ClientOnly({ children, ...delegated }) {
  const [hasMounted, setHasMounted] = React.useState(false);
  React.useEffect(() => {
    setHasMounted(true);
  }, []);
  if (!hasMounted) {
    return null;
  }
  /**
   * Could also replace the <div></div> with
   * <></> and remove ...delegated if no need
   */
  return (
    <div {...delegated}> 
      {children}
    </div>
  );
}

...

<ClientOnly>
  <MyComponent /> // <--- client only stuff, safe to use useLocalStorage in here
</ClientOnly>

or

Custom useHasMounted hook

function useHasMounted() {
  const [hasMounted, setHasMounted] = React.useState(false);
  React.useEffect(() => {
    setHasMounted(true);
  }, []);
  return hasMounted;
}

...

function ParentComponent() {  
  const hasMounted = useHasMounted();
  if (!hasMounted) {
    return null;
  }
  return (
    <MyComponent />
  );
}

...

function MyComponent() {
  const [toleranceH, setToleranceH] = useLocalStorage<number>('toleranceH', 3);
  const [toleranceS, setToleranceS] = useLocalStorage<number>('toleranceS', 3);
  const [toleranceL, setToleranceL] = useLocalStorage<number>('toleranceL', 3);

  ...
}

...

Note:

By overdoing this or using this method at the top level of your component tree, you are killing the Next.js prerendering capabilities and turning your app into more of a "client-side heavy" app (see performance implications). If you are using window.localstorage (outside of components, where you don't have useEffect available), you should always wrap with:

if (typeof window !== 'undefined') {
  // client-side code
}

Upvotes: 10

Related Questions