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:
Logs: Dashboard Route Loads
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.