React - Persist Data on Page Refresh (useEffect, useContext, useState) - reactjs

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

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.

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.

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.

Related

<Suspense> as a root element causes two instances of component in production and dev (no <StrictMode>)

NOTE: this happens in both dev and production, and I'm not using <StrictMode> at all.
It appears to work fine:
B.js
export default function B()
{
return <p>B</p>
}
TestApp.js
import { lazy, Suspense } from "react";
const B = lazy(()=>import("./B"));
export default function TestApp()
{
const counter = useRef(0);
counter.current++;
console.log("rendering TestApp - counter",counter.current);
return <Suspense fallback={<p>loading B...</p>}><B/></Suspense>
}
index.js
import { lazy, Suspense } from "react";
import {createRoot} from "react-dom/client";
const TestApp = lazy(()=>import("./TestApp"));
const root = createRoot(document.getElementById("root"));
root.render(<Suspense fallback={<p>loading testapp...</p>}><TestApp/></Suspense>);
but when there is an additional <Suspense> deeper in the component tree (in TestApp itself, suspending <B>), <TestApp> gets duplicated.
project: https://github.com/jmlee2k/react-suspense-root
demo: https://jmlee2k.github.io/react-suspense-root/ (production build)
To see the issue, go to the demo and open the console, you'll see "rendering TestApp - counter 1" twice. If this was simply a double-render, I would expect the counter to increase.
I'm fairly new to react and am very aware I could be doing something wrong, but any info would be appreciated.
Thanks in advance!
Your application has no unnecessary re-renders; it is how React and closures work.
As explained, the app first loads the suspense component and, once the lazy loading finishes, renders the TestApp component.
In that process, React mounts the TestApp component calling that function. There you have the first invoke count value. When B gets lazily loaded, the function is called again. But there is no re-render, since its state didn't change. All the variables inside the TestApp component got refreshed, except for the useRef one. That is why it is a good idea to have as few handlers inside the component (which gets refreshed on every render) and why it is a good idea to wrap code (aka effects) inside useEffect.
This has something to do with the way react does things. Whenever any component's state or properties are changed, react updates its virtual DOM tree. For example, here you can see how the component is rendered again when there is a change in state
const {useState} = React;
const Example = () => {
console.log("Hi mom!I re-rendered")
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
};
// Render it
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Example />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<div id="root"></div>
Keeping that in mind the flow of your application is the following :
react loads TestApp and the suspense shows the fallback as component B hasn't loaded yet (first render)
Component B finally loads and the suspense displays its content. (second rendering)
so there is nothing wrong with your code because it is the natural behavior of react, however, if there are many unnecessary re-renders it is possible that the application suffers from performance problems.
[Update]
I was looking around and apparently useRef isn't working but I tried console.count and it works now
import { lazy, Suspense, useRef } from "react";
const B = lazy(()=>import("./B"));
export default function TestApp()
{
console.count('counter')
return <Suspense fallback={<p>loading B...</p>}><B/></Suspense>
}

Data on redux store return to initial state

For example, I store navigate on redux store when the app start:
//in App.js file
import { useNavigate } from 'react-router-dom'
const App = () => {
const navigate = useNavigate();
useEffect(() => {
dispatch(addNavigate(navigate))
}, [dispatch, navigate]);
}
And when I use useSelector to select navigate in redux store in another component, it returns undefined. How can I fix it (if I am not use useNavigate directly in this component) and why navigate object became initial state?
From the Redux Docs:
It is highly recommended that you only put plain serializable objects, arrays, and primitives into your store. It's technically possible to insert non-serializable items into the store, but doing so can break the ability to persist and rehydrate the contents of a store, as well as interfere with time-travel debugging.
If you are okay with things like persistence and time-travel debugging potentially not working as intended, then you are totally welcome to put non-serializable items into your Redux store. Ultimately, it's your application, and how you implement it is up to you. As with many other things about Redux, just be sure you understand what tradeoffs are involved.
With all that said, if you still want to add the navigate function from the useNavigate hook you would only be able to get a valid defined value for navigate if it is called in a child component of BrowserRouter from react-router-dom.
So in the top level component this wouldn't work, instead we'd need something like this for your App.js:
import { BrowserRouter } from 'react-router-dom'
import createStore from './createReduxStore'
const store = createStore()
const App = () => (
<BrowserRouter>
<Provider store={store}>
<Content />
</Provider
</BrowserRouter>
)
Then in your Content component you can do what you initially intended:
import { useNavigate } from 'react-router-dom'
const Content = () => {
const navigate = useNavigate();
useEffect(() => {
dispatch(addNavigate(navigate))
}, [navigate]);
}

Keep state while navigating between component in react-router

Let's say I want to have a reusable react component in my project. I also want that component to have its state under different locations without losing it during component unmount. What is the correct way to deal with this kind of architecture in React? In other words, when the user navigates between these two routes react unmounts the previous component, therefore it loads remote data on every navigation between /user and /groups routes.
I also know that there is something called Redux. I don't see a clear way how to do it using reduct. Do I need two reducers? one for Users and the other for Groups? If so it's quite inconvenient creating a new reducer and write new logic each time when I need to use ReusableComponent.
Here is a similar skeleton to describe what I am trying to do. Any hint would be helpful.
//Router example
<Router>
<Switch>
<Route exact path=”/users” >
<UserComponent>
<ReusableComponent url=”http://apidomain.com/users” />
</UserComponent>
</Route>
<Route exact path=”/groups” >
<GroupComponent>
<ReusableComponent url=”http://apidomain.com/groups” />
</GroupComponent>
</Route>
</Switch>
</Router>
//ReusableComponent Example
<ReusableComponent>
--->use url, that passed from parent component tree(users or groups) to load data and keep in state
<ReusableComponentContext>
<Head />
<Body />
<Footer />
</ReusableComponentContext>
</ReusableComponent>
EDIT
So to describe my problem better is I need to have the same component with two or more parallel state on the different locations without overriding each other. If it's possible
I would use the "React Context" api. The context wrappes your app so if one component updates/ rerenderes the state which is stored inside of the context stayes untouched. To use Context you need three files:
"UserContext" = Example => rename!
Context Component (UserContext)
import { createContext } from "react"
export const UserContext = createContext(initValue)
Parent Component (Provider)
//filename: UserContext.js
//* import React, { useState } from "react"
//* import UserContext from "./UserContext"
const [state, setState] = useState("initState")
//* return(
<UserContext.Provider value={{state, setState}}> //value="props"
<ChildComponent/>
</UserContext.Provider>
Child Component (Consumer)
//*import React, { use Context } from "react";
//*import {UserContext} from "./UserContext"
const data = useContext(UserContext) //here "UserContext"
src: short explenation of usage
Edit: consuming with a custom hook
To avoid one import-statement you can create a custom Hook like this
import React, { use Context } from "react";
import {UserContext} from "./UserContext";
const useUserContext = (()=>{
const {state, setState} = useContext(UserContext)
//use effect if you want to set the context? with the hook...
return[state, setState]
})
in your remounting component
import useUserContext from "./useUserContext"
//rfce{
const {state, setState} = useUserContext()
//}
you can connect ReusableComponent to a piece of your redux store (see connect for more details).
import { connect } from "react-redux";
const ReusableComponent = (props) => {
// some logic before return
return <div>{props.magicProperty}</div>
}
const mapStateToProps = (state) => ({ magicProperty: state.magicProperty });
return connect(mapStateToProps)(ReusableComponent);
So every time you use ReusableComponent in you app, the magicProperty is shared, You can also connect some actions to the component in order to manage that part of state in the classical redux flow.
I think I found the solution. In my case, I had some misunderstanding on what level put context provider tag in the router component tree. So in React, it's very important to put the context provider wrapper in the right location. It holds a dedicated state only for those child components that are wrapped by that context provider.
In my case, I had ReusableComponentContext inside ReusableComponent and that was the wrong approach Because everywhere I used ReusableComponent it had individual context(Therefore individual state). I moved ReusableComponentContext on the top of a couple of components to solve my problem.

NEXT JS - How to prevent layout get re-mounted?

Trying next with layout pattern:
https://github.com/zeit/next.js/tree/canary/examples/layout-component
And the problem is that Layout component get remounted on every page change. I need to use layout component as a Container so it'll fetch data from server on every mount. How can I prevent layout to get re-mounted? Or am I missing something there?
This helped me for persistent layouts. The author puts together a function that wraps your page components in your Layout component and then passes that fetch function to your _app.js. This way the _app.js is actually the components that renders the Layout but you get to specify which pages use which layout (in case you have multiple layouts).
So you have the flexibility of having multiple layouts throughout your site but those pages that share the same layout will actually share the same layout component and it will not have to be remounted on navigation.
Here is the link to the full article
Persistent Layout Patterns in Next.js
Here are the important code snippets. A page and then _app.js
// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'
const AccountSettingsBasicInformation = () => (
<div>{/* ... */}</div>
)
AccountSettingsBasicInformation.getLayout = page => (
<SiteLayout>
<AccountSettingsLayout>{page}</AccountSettingsLayout>
</SiteLayout>
)
export default AccountSettingsBasicInformation
// /pages/_app.js
import React from 'react'
import App from 'next/app'
class MyApp extends App {
render() {
const { Component, pageProps, router } = this.props
const getLayout = Component.getLayout || (page => page)
return getLayout(<Component {...pageProps}></Component>)
}
}
export default MyApp
If you put your Layout component inside page component it will be re-remounted on page navigation (page switch).
You can wrap your page component with your Layout component inside _app.js, it should prevent it from re-mounting.
Something like this:
// _app.js
import Layout from '../components/Layout';
class MyApp extends App {
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
return {
...appProps,
};
}
render() {
const { Component, pageProps } = this.props;
return (
<Layout>
<Component {...pageProps} />
<Layout />
);
}
}
export default MyApp;
Also, make sure you replace all the to <Link href=""></Link>, notice that only have change the Html tag to link.
I struggled because with this for many days, although I was doing everything else correctly, these <a> tags were the culprit that was causing the _app.js remount on page change
Even though this is the topic Layout being mounted again and again, the root cause of this problem is that you have some data loaded in some child component which is getting fetched again and again.
After some fooling around, I found none of these problem is actually what Next.Js or SWR solves. The question, back to square one, is how to streamline a single copy of data to some child component.
Context
Use context as a example.
Config.js
import { createContext } from 'react'
export default createContext({})
_App.js
import Config from '../Config'
export default function App({ Component, pageProps }) {
return (
<Config.Provider value={{ user: { name: 'John' }}}>
<Component {...pageProps} />
</Config.Provider>
)
}
Avatar.js
import { useContext } from 'react'
import Config from '../Config'
function Avatar() {
const { user } = useContext(Config)
return (
<span>
{user.name}
</span>
)
}
export default Avatar
No matter how you mount and dismount, you won't end up with re-render, as long as the _app doesn't.
Writable
The above example is only dealing with readable. If it's writable, you can try to pass a state into context. setUser will take care the set in consumer.
<Provider value={useState({})} />
const [user, setUser] = useContext(Config)
setUser is "cached" and won't be updated. So we can use this function to reset the user anytime in child consumer.
There're other ways, ex. React Recoil. But more or less you are dealing with a state management system to send a copy (either value or function) to somewhere else without touching other nodes. I'll leave this as an answer, since even we solved Layout issue, this problem won't disappear. And if we solve this problem, we don't need to deal with Layout at all.

How to detect route changes with react-router v4?

I need to detect if a route change has occurred so that I can change a variable to true.
I've looked through these questions:
1. https://github.com/ReactTraining/react-router/issues/3554
2. How to listen to route changes in react router v4?
3. Detect Route Change with react-router
None of them have worked for me. Is there a clear way to call a function when a route change occurs.
One way is to use the withRouter higher-order component.
Live demo (click the hyperlinks to change routes and view the results in the displayed console)
You can get access to the history object's properties and the closest 's match via the withRouter higher-order component. withRouter will pass updated match, location, and history props to the wrapped component whenever it renders.
https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md
import {withRouter} from 'react-router-dom';
class App extends Component {
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
console.log('Route change!');
}
}
render() {
return (
<div className="App">
...routes
</div>
);
}
}
export default withRouter(props => <App {...props}/>);
Another example that uses url params:
If you were changing profile routes from /profile/20 to /profile/32
And your route was defined as /profile/:userId
componentDidUpdate(prevProps) {
if (this.props.match.params.userId !== prevProps.match.params.userId) {
console.log('Route change!');
}
}
With React Hooks, it should be as simple as:
useEffect(() => {
const { pathname } = location;
console.log('New path:', pathname);
}, [location.pathname]);
By passing location.pathname in the second array argument, means you are saying to useEffect to only re-run if location.pathname changes.
Live example with code source: https://codesandbox.io/s/detect-route-path-changes-with-react-hooks-dt16i
React Router v5 now detects the route changes automatically thanks to hooks. Here's the example from the team behind it:
import { Switch, useLocation } from 'react-router'
function usePageViews() {
let location = useLocation()
useEffect(
() => {
ga.send(['pageview', location.pathname])
},
[location]
)
}
function App() {
usePageViews()
return <Switch>{/* your routes here */}</Switch>
}
This example sends a "page view" to Google Analytics (ga) every time the URL changes.
When component is specified as <Route>'s component property, React Router 4 (RR4) passes to it few additional properties: match, location and history.
Then u should use componentDidUpdate lifecycle method to compare location objects before and after update (remember ES object comparison rules). Since location objects are immutable, they will never match. Even if u navigate to the same location.
componentDidUpdate(newProps) {
if (this.props.location !== newProps.location) {
this.handleNavigation();
}
}
withRouter should be used when you need to access these properties within an arbitrary component that is not specified as a component property of any Route. Make sure to wrap your app in <BrowserRouter> since it provides all the necessary API, otherwise these methods will only work in components contained within <BrowserRouter>.
There are cases when user decides to reload the page via navigation buttons instead of dedicated interface in browsers. But comparisons like this:
this.props.location.pathname !== prevProps.location.pathname
will make it impossible.
How about tracking the length of the history object in your application state? The history object provided by react-router increases in length each time a new route is traversed. See image below.
ComponentDidMount and ComponentWillUnMount check:
React use Component-Based Architecture. So, why don't we obey this rule?
You can see DEMO.
Each page must be wrapped by an HOC, this will detect changing of page automatically.
Home
import React from "react";
import { NavLink } from "react-router-dom";
import withBase from "./withBase";
const Home = () => (
<div>
<p>Welcome Home!!!</p>
<NavLink to="/login">Go to login page</NavLink>
</div>
);
export default withBase(Home);
withBase HOC
import React from "react";
export default WrappedComponent =>
class extends React.Component {
componentDidMount() {
this.props.handleChangePage();
}
render() {
return <WrappedComponent />;
}
};

Resources