Home > Back-end >  React - Persist Data on Page Refresh (useEffect, useContext, useState)
React - Persist Data on Page Refresh (useEffect, useContext, useState)

Time:07-31

React noob here.

I'm trying to use a combination of Context, useState, useEffect, and sessionStorage to build a functioning global data store that persists on page refresh.

When a user logins into the app, I fetch an API and then setUserData when I receive the response.

That part works great, but I realized if a user ever "refreshes" their page that the App resets the userData const to null (and the page crashes as a result).

I studied some articles and videos on using a combination of useEffect and sessionStorage to effectively replace the userData with what is currently in sessionStorage, which seemed like a sensible approach to handle page refreshes. [Source: Login Component Loads

Logs: Dashboard Route Loads

Dashboard Component Logs

Bonus points

Can you help me understand why App Load is being fired more than once (seems it fires 4 times?)

CodePudding user response:

Issue

The useEffect hook, with empty dependency array, runs only once, and since it is outside the router it's completely unrelated to any routing. Additionally, the useEffect hook runs at the end of the initial render, so the wrong initial state value is used on the initial render cycle.

Solution

Initialize the state directly from sessionStorage and use the useEffect hook to persist the local state as it changes.

Example:

function App() {

  console.log("App Load")
  const [userData, setUserData] = useState(() => {
    const user = sessionStorage.getItem("user");
    return JSON.parse(user) || null;
  });

  useEffect(() => {
    console.log("userData updated, persist state");
    sessionStorage.getItem("user", JSON.stringify(userData));
  }, [userData]);

  return (
    ...
  );
}

As with any potentially null/undefined values, consumers necessarily should use a null-check/guard-clause when accessing this userData state.

Example:

const Dashboard = () => {
  const { userData } = useContext(AppContext);

  return (
    <div>
      <Nav />
      <div id='dashboard'>
        <div id='title' className='mt-[38px] ml-[11%]'>
          {userData
            ? (
              <div className='h2'>Good morning, { userData.user_info.first_name }!</div>
            ) : (
              <div>No user data</div>
            )
          }
        </div>
      </div>
    </div>
  );
};

Can you help me understand why App Load is being fired more than once (seems it fires 4 times?)

The console.log("App Load") in App is in the main component body. This is an unintentional side-effect. If you want to log when the App component mounts then use a mounting useEffect hook.

Example:

useEffect(() => {
  console.log("App Load");
}, []); // <-- empty dependency to run once on mount

The other logs are likely related to React's StrictMode component. See specifically Detecting Unexpected Side-effects and Ensuring Reusable State. The React.StrictMode component intentionally double-invokes certain component methods/hooks/etc and double-mounts the component to help you detect logical issues in your code. This occurs only in non-production builds.

CodePudding user response:

What am I not understanding correctly about useEffect? Why does it only fire on the "/" route and not when the page /dashboard is refreshed?

useEffect with empty deps array will run only once when the component is mounted. Your component doesn't unmount when the route change because your router is declared as a child of this component.

Can you help me understand why App Load is being fired more than once (seems it fires 4 times?)

Components rerender every time the state is changed. When a component is rendered all code inside of it is run.

CodePudding user response:

Most likely, you are seeing those multiple console logs due to StrictMode in React, which "invokes" your components twice on purpose to detect potential problems in your application, you can read more details about this here.

I will assume that when you say "refreshing" you mean that if you refresh or reload the browser on the route corresponding to your Dashboard component then this is when the problem arises.

Now, you need to take in consideration that useEffect runs once the component has been mounted on the DOM, and in your Dashboard component, you're trying to access userData which on the first render will be null and then the useEffect will fire and populate the state with the data coming from your sessionStorage. How to fix this? Add optional chaining operator in your Dashboard component like so:

{ userData?.user_info?.first_name }

Additionally, I would suggest you to move your userData state and the useEffect logic in your App to your AppContext (in a separate file). Let me know if this works for you.

CodePudding user response:

I prefer you make use of redux persistent for better experience and implementation.

  • Related