2

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: React Persist State to LocalStorage with useEffect by Ben Awad]

However, I can't seem to get the useEffect piece to work. For some reason useEffect is only firing on the / route and not on the /dashboard route?

I would expect because the useEffect is in App.js that it runs every time any route is refreshed (thus retrieving the latest data from sessionStorage).

I added some console logging to App.js for when events are being fired and included those logs below.

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

App.js

import { useState, createContext, useEffect } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import '../css/app.css';
import Login from './auth/login';
import Dashboard from './dashboard/dashboard';

export const AppContext = createContext(null);

function App() {

  console.log("App Load")
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    console.log("Use effect ran");
    const user = sessionStorage.getItem("user");
    if (user) {
      setUserData(JSON.parse(user));
      console.log("Retreived session storage");
    }
  },[]);

  return (
      <AppContext.Provider value={ { userData, setUserData } }>
        <BrowserRouter>
          <div className="App">
              <Routes>
                <Route path="/" element={<Login />}></Route>
                <Route path="/dashboard" element={<Dashboard />}></Route>
              </Routes>
          </div>
        </BrowserRouter>
      </AppContext.Provider>
  );
}

export default App;

Dashboard component

import { useContext } from 'react'
import '../../css/app.css'
import { AppContext } from '../app';
import Nav from '../../components/primary-nav';

const Dashboard = () => {

    const { userData } = useContext(AppContext);

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

export default Dashboard;

I would have expected useEffect() fires when the Dashboard page is refreshed, but here are the logs respectively.

Logs: Default Route Loads

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?)

3 Answers 3

5

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Excellent! Thanks for this great answer! That makes a ton of sense to initialize the state directly from sessionStorage. I'm curious and since I'm learning, in your opinion is this an efficient way to manage global storage?
@Parker Managing persisting global app state? Sure, absolutely. It's common to persist React state to local/session storage and initialize your state from it.
Cool. Thanks so much for the great insights and support! It's truly appreciated.
@DrewReese But I did find some sites persist states, for example, search filter data not in local/Session Storage. And after I refresh the page the state stays unchanged. I look at all the storage space and they are always empty. I didn't even login to the site. What possible methods do they use to persist the states?
1

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.

1 Comment

Thanks for that explanation. First it was very helpful to get my head around components rerendering every time the state is changed. Second, that makes sense that the useEffect with empty array will only run once when the component is mounted. In my use case, do you have a recommendation for where the best place to retrieve the sessionStorage would be? Seems duplicative to retrieve it within each component?
1

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.

3 Comments

You rock. That explanation was extremely clear, and yes to "refreshing" I meant if the user reloads the browser. Adding the optional chaining did the trick! Can you explain the benefit of moving the userData and useEffect to a separate file?
Sure, it's a matter of good practices and structure, this of course has some personal opinion factor on it. It's generally a better idea to separate your Context into a different file, take in consideration that it could get really complex and really messy on different scenarios, maybe not in your case, but it can happen. I think it's a good habit.
Thank you my friend. Truly appreciate the great answers and mentorship! Definitely learned a lot.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.