How to fetch data before route change? - reactjs

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>}

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.

React JS page keeps refreshing when using the back button

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

Redirect to another component after form submission in reactjs

I have the following method to handle form submission-
handleSubmit(event) {
event.preventDefault();
axios.get('url').then(response => {
if(response.data.issuccess){
this.props.history.push("/SuccessComponent");
}
});
}
It changes the URL in addressbar, but UI remains the same.
How to solve this?
This method will not work perfectly.
If you're using react-router-dom package, you need to initially set the route of your successCompoment in the root file mostly app.js.
import SuccessComponent from .....link to the file...
<Route path='/SuccessComponent' component={SuccessComponent} exact/>
Applying this will work.
If you didn't define the route to a component, you cannot Make a navigate to a URL expecting to render different component not initially defined

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.

Are my expectations about React navigation correct?

It might be my lack of understanding, but would anyone explain if my expectations are wrong about React navigation?
My app is more or less a questionnaire where one screen handles all questions. Basically it navigates to itself with a new question ID. That fetches the proper data and displays it. Working fine, but when I press back I do not go to the previous question but to the home page. I use expo and their recommended navigation.
My expectation is when I go back from the last page in :
Homepage => QuestionPage(id=1) => QuestionPage(id=3)
I would go back to QuestionPage with id = 1, but it goes to Homepage. I use withNavigation on both pages to maintain the navigation props.
Is this expectation wrong or is this correct navigation behavior? If so, any clues what to do to get my expected behavior.
To achieve react navigation multi-page application from single page. You need to add "react-router-dom" package in your project.
import {
Route as Router,
Switch,
Redirect
} from 'react-router-dom';
You have to setup router for navigate page as follow.
<Switch>
<Router exact path="/questionpage/:handle" component={Questionpage} />
</Switch>
In Question page Router :
this.props.match.params.handle // will help to get page number
By using above syntax you can get page number and able to get data exact you want.
It was so simple. Instead of
this.props.navigation.navigate('Question', {id=40})}
I had to write
this.props.navigation.push('Question', {id=40})}
If you're using a StackNavigator and from QuestionPage on the back button you do
this.props.navigation.goBack();
instead of
this.props.navigation.navigate('Screen')}
it should work !
If you want to have a custom navigation on a react navigation screen, on the backAndroid I'd recommend to listen to the didFocus like this:
_didFocusSubscription;
_willBlurSubscription;
constructor(props) {
super(props);
this._didFocusSubscription = props.navigation.addListener('didFocus', payload =>
BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
componentDidMount() {
this._willBlurSubscription = this.props.navigation.addListener('willBlur', payload =>
BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
hardwareBackPress = () => { this.props.navigation.navigate('Screen')} }
A more accurate example and the documentation regarding it is here https://reactnavigation.org/docs/en/custom-android-back-button-handling.html

Resources