Next.js Discord

Discord Forum

How To Use localStorage With State Initializer?

Unanswered
Australian Freshwater Crocodile posted this in #help-forum
Open in Discord
Australian Freshwater CrocodileOP
Hello,

I am trying to grab data from localStorage and use it to change the text in a button. This system should detect if a user has clicked the button before and say "continue" instead of "start." I made a useUserHistory hook to wrap this behaviour.

import { useEffect, useReducer, useState } from "react";

export type UserHistoryObject = {
  [key: string]: string;
};

export interface UserHistory {
  userHistory: UserHistoryObject;
  setUserHistory: (pv: UserHistoryObject) => void;
  getBoolean: (item: string) => boolean;
  setBoolean: (item: string, value: boolean) => void;
  getNumber: (item: string) => number;
  setNumber: (item: string, value: number) => void;
  getString: (item: string) => string;
  setString: (item: string, value: string) => void;
  clearUserHistory: () => void;
}

export default function useUserHistory(): UserHistory {
  const [userHistory, setUserHistory] = useState({} as any);

  useEffect(() => {
    if (
      localStorage.getItem("userHistory") &&
      Object.keys(userHistory).length === 0
    ) {
      setUserHistory(JSON.parse(localStorage.getItem("userHistory") as string));
    }
  }, []);

  useEffect(() => {
    if (
      localStorage.getItem("userHistory") &&
      Object.keys(userHistory).length === 0
    )
      return;
    localStorage.setItem("userHistory", JSON.stringify(userHistory));
  }, [userHistory]);

  return {
    userHistory,
    setUserHistory,
    getBoolean: (item) => userHistory?.[item] === "true" || false,
    setBoolean: (item, value) => {
      const newUserHistory = { ...userHistory };
      newUserHistory[item] = String(value);
      setUserHistory(newUserHistory);
    },
    getNumber: (item) => Number(userHistory?.[item]),
    setNumber: (item, value) => {
      const newUserHistory = { ...userHistory };
      newUserHistory[item] = String(value);
      setUserHistory(newUserHistory);
    },
    getString: (item) => userHistory?.[item],
    setString: (item, value) => {
      const newUserHistory = { ...userHistory };
      newUserHistory[item] = value;
      setUserHistory(newUserHistory);
    },
    clearUserHistory: () => {
      setUserHistory({});
    },
  };
}


Only this version results in no error, but it always flashes "Start" until the value is retrieved.
When I use

const [userHistory, setUserHistory] = useState(JSON.parse(localStorage.getItem("userHistory") || "{}"));

and remove the first useEffect, it seems to work, but I do get a error for localStorage not being defined.
I understand this is due to the server pre-rendering the initial value of useState, a behaviour I do not want in this case.

Should I rethink this entire system?

2 Replies

Birman
Why not not render the button until the state has held a value
Australian Freshwater CrocodileOP
That was what I was thinking, but it would look kind of stupid to not render something basic until after hydration. I think this is probably the solution I am going to implement, but it is unfortunate there is not a better way to acomplish this.