React Router v4 replace history when component is unmounted - reactjs

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.

Related

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

is there a faster way to check the user's state in firebase like onAuthStateChanged?

I have currently working on a next.js project using react, redux and firebase.
When user enters a page that need authorization I use the following code to redirect them if they are not authenticated.
import React from 'react';
import Router from 'next/router';
import { firebase } from '../../firebase';
import * as routes from '../../constants/routes';
const withAuthorization = (needsAuthorization) => (Component) => {
class WithAuthorization extends React.Component {
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!authUser && needsAuthorization) {
Router.push(routes.SIGN_IN)
}
});
}
render() {
return (
<Component { ...this.props } />
);
}
}
return WithAuthorization;
}
export default withAuthorization;
I am using the following repo as my base project. The problem is the app seems to work fine but when I try to navigate to a page that requires authentication when I am not authenticated. I am not redirected immediately rather it shows the page first then redirects. Since the HOC uses
firebase.auth.onAuthStateChanged()
which is asynchronous. Is there a faster way of checking if the user is logged in.
I have so far considered the
firebase.auth().currentUser
but my internal state depends on the update of the onAuthStateChanged function when the user logs out in another page.
Using a listener to onAuthStateChanged is usually the correct way to restore authentication state upon a full page transition. Since there is a reload happening at such a transition, there is a chance that the authentication state has changed. Using an onAuthStateChanged listener ensures that Firebase calls your code after it has validated the authentication state of the user. Since this may require a call to the server, this can indeed takes some time, so you'll want to show a "loading..." animation.
If the state transition doesn't require a reload, like when you're just rendering a new route in the same page, then you can be reasonably certain that the authentication state doesn't change on the transition. In that case you could pass the current user from the previous component into the new one. Tyler has a good article on this: Pass props to a component rendered by React Router.
In the latter case firebase.auth().currentUser should also retain its state, and you can use it safely. You'd then still use an onAuthStateChanged to detect changes in authentication state after the new route has loaded.

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

How to refresh a component after already visiting it?

Working on a project and when changing routes, coming back to one after already visiting it doesnt display new changes/additions. For example, if i route to my "Offers" route, it will display my offers on current items for rent. If i then leave that route and go and place another offer on an item, routing back to "Offers" will reveal no changes unless I refresh the page. I populate the state in componentdidmount() but have also tried other ways where im not using componentdidmount() to populate it. What options do I have here? I can post the component if needed.
Going back to a route which was already visited in the same BrowserRouter context, will not MOUNT the component again. It would only cause the router to perform a pop action, which causes the browser to pop the latest DOM generated for that route to pop from the router context. So what you would have to do, is to check if the user has pressed back button in his/her browser. To do so, you can check this.props.history.action === 'POP' in componentWillReceiveProps lifecycle. Something like this:
class Offers extends Component {
constuctor (props) {
super(props);
this.state = {
offers: []
}
}
componentDidMount () {
this.fetchOffersAndStoreThemInState();
}
componentWillReceiveProps () {
if (this.props.history.action === 'POP') {
this.fetchOffersAndStoreThemInState();
}
}
}
And don't forget to wrap your component with withRouter to have access to the history object.

Resources