React - Persisting Data on Page Refresh

jucheng925

Julie Cheng

Posted on May 15, 2024

React - Persisting Data on Page Refresh

It's been nearly a year since I wholeheartedly delved into learning programming and coding and one thing has become abundantly clear. There are myriad approaches to achieve the same outcome. However, each method carries its own set of advantages and disadvantages, whether it adheres to conventional wisdom or prioritizes memory efficiency. As a beginner, no matter which methods were used, seeing a functioning end product without noticeable bugs fills me with immense pride in what I've achieved.

Now, as I dive into my second full-stack project (with React powering the frontend and Flask handling the backend), I find myself confronted with a familiar challenge. To be a proficient developer, it's clear that I need to move past quick fixes and seek more sophisticated solutions.

In my project, user authentication is implemented, with user data stored in global context through UseContext. Below is a simplified example reflecting only a portion of my project.



//UserContext.jsx
const UserContext = createContext({});

const UserProvider = ({children}) => {
    const [currentUser, setCurrentUser] = useState(null);

    const login = (user) => {
      setCurrentUser(user)
    }

    const logout = () => {
      setCurrentUser(null))
    }

    return (
        <UserContext.Provider 
          value={{currentUser, login, logout}}>
            { children }
        </UserContext.Provider>
      )
  }

export { UserContext, UserProvider }



Enter fullscreen mode Exit fullscreen mode


//Home.jsx
const Home = () => {
  const { currentUser } = useContext(UserContext)

  return (
    <div className='home'>
       <p> Welcome, {currentUser.username}! </p>
       <div className='posts'>
           <h4>Here are your posts</h4>
           {currentUser.posts.map((post) => (
               <PostCard key={post.id} post={post}/>
           ))}
        </div>
    </div>

export default Home



Enter fullscreen mode Exit fullscreen mode

Alright, everything seems fine thus far. However, what arises as an issue is when the web page is refreshed. At that point, our entire page crashes, presenting a series of errors such as unable to map an undefined value or cannot read properties of null.

errormessage

What's happening? Why is currentUser undefined? Just to provide some context, currentUser is saved in the backend by assigning the session cookie with the user id and a fetch is made through useEffect to check the session cookie during each rerender.

Firstly, it's important to note that a fetch to the backend will inherently involve some time before a response is received. Secondly, when the page is refreshed, the state value reverts to its initial state. In this case, it returns to null. Given the slight delay in retrieving currentUser from the backend, the Home component cannot render because currentUser is not yet defined.

Below is my quick band-aid fix.



//Home.jsx
const Home = () => {
  const { currentUser } = useContext(UserContext)

  return (
    <div className='home'>
       {currentUser ? 
         <>
           <p> Welcome, {currentUser.username}! </p>
           <div className='posts'>
              <h4>Here are your posts</h4>
              {currentUser.posts.map((post) => (
                 <PostCard key={post.id} post={post}/>
              ))}
           </div>
        </>  : null}
    </div>

export default Home



Enter fullscreen mode Exit fullscreen mode

Since we only require a few microseconds before receiving our response with the currentUser, simply adding a conditional statement can prevent the error from crashing everything, and voilà! No more error messages.

fixed

Great! However, I don't want to have to implement this for all my components that require currentUser. Additionally, even though I find it difficult, I am striving to adhere to the principle of keeping my code as DRY (don't repeat yourself) as possible.

Let's turn to our reliable source for all things—Google. Interestingly, web browsers come equipped with build-in web storage, where data can be store and retrieve as needed. There are two types: localStorage and sessionStorage. Data stored in localStorage does not expire, while the data stored in sessionStorage is cleared when the page session ends, that is when the browser tab closes. It's important to note that this data is stored in JSON format.

Now, armed with this new information, let's examine the revised code below to address the issue of data not persisting after a page refresh.



//UserContext.jsx
const UserContext = createContext({});

const getInitialState = () => {
  const currentUser = sessionStorage.getItem("currentUser");
  return currentUser ? JSON.parse(currentUser) : null
}

const UserProvider = ({children}) => {
  const [currentUser, setCurrentUser] = useState(getInitialState);

  useEffect(() => {
      sessionStorage.setItem("currentUser", JSON.stringify(currentUser))
  }, [currentUser])

  const login = (user) => {
    setCurrentUser(user)
  }

  const logout = () => {
    setCurrentUser(null))
  }

  return (
      <UserContext.Provider 
        value={{currentUser, login, logout}}>
          { children }
      </UserContext.Provider>
    )
}

export { UserContext, UserProvider }



Enter fullscreen mode Exit fullscreen mode

A new function getInitialState was defined. When called, it attempts to retrieve the currentUser value from sessionStorage. If the currentUser value exists, it will return the parsed JSON object; otherwise, it returns null.

Upon closer examination, the getInitialState function is invoked during the initial setup of the currentUser state. This means that when a page is refreshed and the state needs to be reinitialized, the initial value of the state won't necessarily be null. It will depend on whether there is available data in sessionStorage.

Now, with currentUser persisting after a refresh, are we done? Not quite. There's one more scenario to consider.

Consider this: currentUser isn't a static value. It changes with every login and logout. sessionStorage only knows about the first user who logged in. But what if that user logs out and a second user logs in? Upon page refresh, the information of the first user shows up, not the second user's. This poses a security breach that we must address.



  useEffect(() => {
      sessionStorage.setItem("currentUser", JSON.stringify(currentUser))
  }, [currentUser])


Enter fullscreen mode Exit fullscreen mode

That's why we implemented this useEffect with a dependency array containing currentUser. Whenever currentUser changes due to a through login or logout, the asynchronous function within UseEffect runs. Instead of "getting" the data from sessionStorage as seen as before, this function is "setting" or replacing the currentUser data stored in sessionStorage with the JSON stringified verison of the newly updated currentUser data.

With these changes in place, refreshing the page won't trigger any errors, and data persists as expected. Great job! We've successfully resolved the issue.

highfive

Hope you find this interesting as I did. Happy coding!

References:
How to Save React State to Local Storage - YOUTUBE

How to persist React states after a refresh?!

5 Methods to Persisting State Between Page Reloads in React

💖 💪 🙅 🚩
jucheng925
Julie Cheng

Posted on May 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related