React JS page keeps refreshing when using the back button - reactjs

I have 2 React JS pages (A & B), when I go from A->B and back to A, page A is refreshed every time. I was under the impression that page is not destroyed. All related questions on StackOverflow seems to be about the opposite problem.
The reason the page refreshes is because useEffect() is called when the back button is pressed despite using useState() to prevent this. I even tried replacing 'refresh' with a 'props.id' parameter (that never changes). See code below:
Here's my code to page A:
import { useHistory, useParams } from "react-router-dom";
import React, { useState, useEffect, useRef } from "react";
import { Link } from "react-router-dom";
export default function Test(props) {
const [refresh, setRefresh] = useState(false);
useEffect(() => {
console.log("useEffect called: "+refresh);
setRefresh(true);
},[refresh]);
return (
<>
Hello from Test
<Link to="/test2">Test me</Link>
</>
);
}
I'm using react-router-dom: "^5.1.2", and import { BrowserRouter as Router } from "react-router-dom"; in App.js and specified:
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/test">
<Test id="1"/>
</Route>
<Route exact path="/test2">
<Test2 />
</Route>
.....
Does anyone know how to prevent useEffect() from being triggered when returning to page? The actual page A fetches using a REST call and display a long list of items and I do not want the page to refresh every time the user load page B to view item and then returns to the page.

You need to add a condition to useEffect.
If you only want to setRefresh to true if its false, then do something like:
useEffect(() => {
if(!refresh) setRefresh(true)
}, [refresh])
Since you are starting with const [refresh, setRefresh] = useState(false) and are not changing refresh anywhere else in the component, this will run once everytime the component loads (not renders).
If you want to run this once in the lifetime of the app and not the component, you need to persist the information outside the component, by either lifting the state up to a parent component and persisting the information is something like localstorage/sessionstorage.
You could then extract this information whenever your component loads and set the refresh state variable accordingly.
Let's say you just want to setRefresh to true once. Add this useEffect:
useEffect(() => {
let persistedRefresh
try {
persistedRefresh = !!JSON.parse(window.localstorage.getItem('THE_KEY_TO_REFRESH_VALUE'))
} catch(error) {
persistedRefresh = false
}
setRefresh(persistedRefresh)
}, [])
This useEffect will run whenever the component loads, and update the state variable, triggering the previous useEffect.
We also need to modify the previous useEffect:
useEffect(() => {
if(!refresh) {
setRefresh(true)
window.localstorage.setItem('THE_KEY_TO_REFRESH_VALUE', JSON.stringify(true))
}
}, [refresh])
In this useEffect we are updating the persisted value so that whenever the component loads,
it will check the persisted value,
refresh if needed, and
update the persisted value for the next loads.
This is how you do it without any extra dependencies.

I can see that you're importing the very useful useHistory prop, but not doing much with it. It can actually be used to check if a user is navigating to the page by using the back button. useHistory()'s action properly will tell you everything you need. If the back button was used, action will be "POP". So you can put some logic into your useEffect to check for that:
const history = useHistory();
React.useEffect(() => {
if (history.action === "POP")
console.log("Back button used. Not running stuff");
else console.log("useEffect called in home");
}, []);
Here is a Sanbox. And here you can actually test the sandbox code in a dedicate browser window: https://okqj3.csb.app/
Click the "About" link and then use the back button to go back to "Home", in the console you will see how the Home element's useEffect function catches it.

Solution 1 (Correct way)
Use Stateless components and have a common super state (Redux will be of great assistance), and bind you page/data to common state so even if the state changes, the page will always render the current state creating an illusion of page retaining the state (I used it to run large queries and store progress/result in redux so even if I open another page and come back then also I see query in progress or result).
However I am not really sure what your use case is.
Solution 2 (slightly wrong way)
Use React.memo,You can use it when you don't want to update a component that you think is static
For function Components:
const Mycomponents = React.memo(props => {
return <div>
No updates on this component when rendering, use useEffect to verify too
</div>;
});
You shouldn't be defining any method/functionality/dynamic calculation inside this kind of method just to avoid getting irregular data

Related

Move between pages without losing pages content - React JS

I'm trying to navigate between pages without losing pages data. For instance, I have a page that contains many input fields, if a user filled all these fields and tried to move to another page and return back to the first one, all the inputs are gone. I am using react-router-dom but didn't find out a way to prevent that.
What I've done till now :
import { Route, Switch, HashRouter as Router } from "react-router-dom";
<Router>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/hello-world" exact component={HellWorld} />
</Switch>
</Router>
Home Component :
navigateToHelloWorld= () => {
this.props.history.push('/hello-world')
};
Hello World Component :
this.props.history.goBack();
I don't know that that can be supported in such generality. I would just store all state variable values in localStorage, and restore from there when values are present on component render (when using useState then as the default value). Something like this:
const Home = () => {
const [field, setField] = useState(localStorage.field || '');
const handleUpdate = (value) => {
setField(value);
localStorage.field = value;
}
// also add a submit handler incl. `delete localStorage.field`;
return ... // your input fields with handleUpdate as handler.
}
Generally, all you need to do is to store your data in someplace, either a component that doesn't unmount, like the component you are handling your routes in, which is not a good idea actually but it works!
another way is to use some kind of state manager like 'mobx','redux','mst', or something which are all great tools and have great documentation to get you started
another alternative is to store your data in the browser, for your example session storage might be the one to go for since it will keep data until the user closes the tab and you can read it in each component mount.

How to fetch data before route change?

I'm trying to have the following user flow when a user click on a link:
The user clicks on a link
A progress bar appears at the top of the page
The JS launches a network request to fetch some data from the server
When done, the progress bar finishes, and the page is switch
Note that I don't want to have any spinner or skeleton page. When the user clicks on the link, the page should not change at all (apart from the progress bar appearing) until the data has been fetched from the server, similar to how GitHub works.
I've searched a lot about this on the last few days, and it seems that it's not possible to do this:
Apparently, there used to be a onEnter hook that made it possible to achieve my described flow, but it was removed because, according to the devs, React lifecycle hooks were enough to achieve this.
React lifecycle hooks are not enough because if I use them to trigger the network request, the page will be blank between the click on the link and the response of the network request.
I could make a wrapper on top of the Link component so that when the user clicks on it, the network request is triggered and only after it's finished, router.navigate would be called. It seems nice at first, but it doesn't solve the issue of the initial visit to a page, where a Link button has not been called at all.
Any ideas on how to achieve this?
Thanks in advance!
I created a workaround for such behaviour: react-router-loading. It allows you to fetch data before switching the page.
You only need to replace Switch / Routes and Route with ones from the package, mark some (or all) routes with the loading prop and tell the router when to switch pages using the context in components:
import { Routes, Route } from "react-router-loading";
<Routes> // or <Switch> for React Router 5
<Route path="/page1" element={<MyPage1 />} loading />
<Route path="/page2" element={<MyPage2 />} loading />
...
</Routes>
// MyPage1.jsx
import { useLoadingContext } from "react-router-loading";
const loadingContext = useLoadingContext();
const loading = async () => {
// fetching data
// call method to indicate that fetching is done and we are ready to switch pages
loadingContext.done();
};
write a onClick function for your component
then function like this
import { useHistory } from "react-router-dom";
const history=useHistory();
const [loading,setLaoding]=React.useState(false);
const myfunction=async()=>{
setLoading(true);
const res= await fetch("your link here");
const data=res.json();
if(res.status===200)
{
console.log(succusfully fetch data)
setLoading(false);
history.push("/your_destination");
}
else{
setLoading(false);
console.log("error in fetch data")
}
}
write link like this
{loading?<Spin/> :
<p onClick={myfunction}>link</p>}

React useEffect strange behaviour with custom layout component

I'm trying to use scroll position for my animations in my web portfolio. Since this portfolio use nextJS I can't rely on the window object, plus I'm using navigation wide slider so I'm not actually scrolling in the window but in a layout component called Page.
import React, { useEffect } from 'react';
import './page.css';
const Page = ({ children }) => {
useEffect(() => {
const scrollX = document.getElementsByClassName('page')
const scrollElement = scrollX[0];
console.log(scrollX.length)
console.log(scrollX)
scrollElement.addEventListener("scroll", function () {
console.log(scrollX[0].scrollTop)
});
return () => {
scrollElement.removeEventListener("scroll", () => { console.log('listener removed') })
}
}, [])
return <div className="page">{children}</div>;
};
export default Page;
Here is a production build : https://next-portfolio-kwn0390ih.vercel.app/
At loading, there is only one Page component in DOM.
The behaviour is as follow :
first listener is added at first Page mount, when navigating, listener is also added along with a new Page component in DOM.
as long as you navigate between the two pages, no new listener/page is added
if navigating to a third page, listener is then removed when the old Page is dismounted and a new listener for the third page is added when third page is mounted (etc...)
Problem is : when you navigate from first to second, everything looks fine, but if you go back to the first page you'll notice the console is logging the scrollX value of the second listener instead of the first. Each time you go on the second page it seems to add another listener to the same scrollElement even though it's not the same Page component.
How can I do this ? I'm guessing the two component are trying to access the same scrollElement somewhat :/
Thanks for your time.
Cool site. We don't have complete info, but I suspect there's an issue with trying to use document.getElementsByClassName('page')[0]. When you go to page 2, the log for scrollX gives an HTMLCollection with 2 elements. So there's an issue with which one is being targeted. I would consider using a refs instead. Like this:
import React, { useEffect, useRef } from 'react';
import './page.css';
const Page = ({ children }) => {
const pageRef = useRef(null)
const scrollListener = () => {
console.log(pageRef.current.scrollTop)
}
useEffect(() => {
pageRef.addEventListener("scroll", scrollListener );
return () => {
pageRef.removeEventListener("scroll", scrollListener )
}
}, [])
return <div ref={pageRef}>{children}</div>;
};
export default Page;
This is a lot cleaner and I think will reduce confusion between components about what dom element is being referenced for each scroll listener. As far as the third page goes, your scrollX is still logging the same HTMLElement collection, with 2 elements. According to your pattern, there should be 3. (Though there should really only be 1!) So something is not rendering properly on page 3.
If we see more code, it might uncover the error as being something else. If refs dont solve it, can you post how Page is implemented in the larger scope of things?
also, remove "junior" from the "junior developer" title - you won't regret it

React - sharing variables through useContext, to update and re-render components, but not working

I am creating an exercise app (using MERN stack and graphql)
where it takes keywords from the user and fetches youtube videos matching those keywords.
After fetching the youtube data, I save that exercise in my mongoDB database, and also saved them in an array exerciseArr. This exerciseArr is a shared through different react components, through useContext.
The fetching request and receiving data happens in my ExerciseForm component.
I am using ExerciseList component to render those fetched exercises(saved in exerciseArr), whenever there is an update in exerciseArr, it should re-render items in exerciseArr to ExerciseVideo. That is happening in useEffect of ExerciseList component.
useEffect(() => {
console.log('exerciseArr in ExerciseList', exerciseArr)
const fetchedExercises = exerciseArr.map( exercise => (
<ExerciseVideo key={exercise._id}
id={exercise._id}
bodysection={exercise.bodysection}
duration={exercise.duration}
title={exercise.title}
videoUrl={exercise.videoUrl}
favorite={exercise.favorite}/>))
setVideoElements(fetchedExercises)
}, [exerciseArr])
The issue now is that ExerciseList component only renders with 1st request from ExerciseForm, and it does not re-render, unless you initiate some action(click event etc) with the already rendered video elements which is the prat of the ExerciseList component.
My understanding is that every time there is an update of exerciseArr, useEffect in ExerciseList would re-render. But this is not happening?
To force to re-render the ExrciseList component, I need to initiate click event or any sorts, with already existing ExerciseVideo elements which is part of the ExerciseList component.
This is how I render ExerciseList component in App
....
function App() {
const { isAuth, exerciseArr } = useContext(Context)
const hasExercise = exerciseArr.length > 0
// const authenticatedView = <><ExerciseForm/></>
return (
<div className="App">
<Switch>
<Route exact path="/">
<NavBar />
<Manual />
{ isAuth ? <ExerciseForm/> : <Login/> }
{ isAuth && hasExercise ? <ExerciseList /> : ''}
</Route>
....
I wonder if I am not using useEffect in ExerciseList component correctly, in order to re-render itself. Or how I am rendering ExerciseList component in App needs to change.
Can somebody help me with this issue?
I found the error which was causing the problem.
The useEffect function is correct, but the if statement where it should render and update the exerciseArr was returning false.
if(resData.data.exercises.__typename === 'ExerciseData') {
//then save those results in exerciseArr
const graphqlResponse = resData.data.exercises.exercises
return setExerciseArr(graphqlResponse)
}
this block's resData.data.exercises.__typename path was not correct before.
Thus not updating the exerciseArr at all.
*sidenote, I moved this useEffect function into Context and only render exerciseArr in ExerciseList to create video div elements now, to make component more simple.

React Router v4 replace history when component is unmounted

I have a app which has the Following Components
|__Base - /home/Base
|__Try - /home/Base/:try
|__Report - /home/Base/:try/report
Base is the Starting screen where the user hits a button and clicks on Try and after trying some things he hits submits which generates reports which has some back end interactions and when the data is fetched it loads the Reports.
So what i want is when the user hits the back button from the Reports Page he should not land on the Try page but on the Base page .
For that to work i went through the react router documentation and was trying to use history.replace on componentWillUnmount for Reports Page
this.props.history.replace(`/home/Base`, {
pathname: `/home/Base`,
search: null,
state: {
isActive: true
}
}, null);
In case the Report Page is FullyLoaded and i press the back button it works but calls the Try Render Method too and then takes me to the Base Page , But in case of Reports Not fully Loaded and i press the back button while the loading spinner is in progress it goes to base page still but also mounts and unmounts the TRY component.
What am i missing here , what causes it to mount/unmount or render the previous component and then load the base component even though i replace the history stack ?
Reason
Related with this issue
React v16, changing routes, componentWillMount of the new route is called before componentWillUnmount of the old route
Update:
Solution (checked, update online demo later)
Use react-router-last-location to get previous pathname
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
import { LastLocationProvider } from 'react-router-last-location';
<BrowserRouter>
<LastLocationProvider>
<Switch>
...
</Switch>
</LastLocationProvider>
</BrowserRouter>
Check previous pathname in componentWillMount, if it's from certain page, push a new pathname to route.
componentWillMount() {
const { history, lastLocation } = this.props;
if (lastLocation?.pathname === '/home/Base/:try/report') {
history.push({pathname: '/home/Base'});
}
}
You can use the HOC they provide or write it yourself refer to the lib's source to reduce the dependencies
import { withLastLocation } from 'react-router-last-location';
interface Props {
lastLocation: any,
history: any,
}
export const YourComponent = withLastLocation(connect(
...
))
In this way you can redirect all the routing process from certain pages without mount current page, no matter you clicked a back button or clicked the back in your browser.

Resources