Check New Route on componentWillUnmount? - reactjs

I have some code that will run on componentWillUnmount() but I only want it run if they go back to the previous page. If they go forward to the next page I don't want what is inside the componentWillUnmount to run.
I am using React Router 4 but when I check it in the componentWillUnmount it still has not updated to whatever the next url is.
componentWillUnmount() {
const props = this.props;
const location = props.location;
}

React Router provides a history object which you can use to set some variables before the transition to a new location.
Try something like this:
componentDidMount() {
this.props.history.block((location, action) => {
this.isGoingBack = action === 'POP';
})
}
componentWillUnmount() {
if (this.isGoingBack) {
...
}
}
You might need to check the location aswell.

Related

How do I call props.history.push() if I'm destructuring my props?

If I've got a function that creates a confirm popup when you click the back button, I want to save the state before navigating back to the search page. The order is a bit odd, there's a search page, then a submit form page, and the summary page. I have replace set to true in the reach router so when I click back on the summary page it goes to the search page. I want to preserve the history and pass the state of the submitted data into history, so when I click forward it goes back to the page without error.
I've looked up a bunch of guides and went through some of the docs, I think I've got a good idea of how to build this, but in this component we're destructuring props, so how do I pass those into the state variable of history?
export const BaseSummary = ({successState, children}: BaseSummaryProps) => {
let ref = createRef();
const [pdf, setPdf] = useState<any>();
const [finishStatus, setfinishStatus] = useState(false);
const onBackButtonEvent = (e) => {
e.preventDefault();
if (!finishStatus) {
if (window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?")) {
setfinishStatus(true);
props.history.push(ASSOCIATE_POLICY_SEARCH_ROUTE); // HERE
} else {
window.history.pushState({state: {successState: successState}}, "", window.location.pathname);
setfinishStatus(false);
}
}
};
useEffect(() => {
window.history.pushState(null, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
Also I'm not passing in the children var because history does not clone html elements, I just want to pass in the form data that's returned for this component to render the information accordingly
first of all, I think you need to use "useHistory" to handling your hsitry direct without do a lot of complex condition, and you can check more from here
for example:
let history = useHistory();
function handleClick() {
history.push("/home");
}
now, if you need to pass your history via props in this way or via your code, just put it in function and pass function its self, then when you destruct you just need to write your function name...for example:
function handleClick() {
history.push("/home");
}
<MyComponent onClick={handleClick} />
const MyComponent = ({onClick}) => {....}
I fixed it. We're using reach router, so everytime we navigate in our submit forms pages, we use the replace function like so: {replace: true, state: {...stateprops}}. Then I created a common component that overrides the back button functionality, resetting the history stack every time i click back, and using preventdefault to stop it from reloading the page. Then I created a variable to determine whether the window.confirm was pressed, and when it is, I then call history.back().
In some scenarios where we went to external pages outside of the reach router where replace doesn't work, I just used window.history.replaceStack() before the navigate (which is what reach router is essentially doing with their call).
Anyways you wrap this component around wherever you want the back button behavior popup to take effect, and pass in the successState (whatever props you're passing into the current page you're on) in the backButtonBehavior function.
Here is my code:
import React, {useEffect, ReactElement} from 'react';
import { StateProps } from '../Summary/types';
export interface BackButtonBehaviorProps {
children: ReactElement;
successState: StateProps;
}
let isTheBackButtonPressed = false;
export const BackButtonBehavior = ({successState, children}: BackButtonBehaviorProps) => {
const onBackButtonEvent = (e: { preventDefault: () => void; }) => {
e.preventDefault();
if (!isTheBackButtonPressed) {
if (window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?")) {
isTheBackButtonPressed = true;
window.history.back();
} else {
isTheBackButtonPressed = false;
window.history.pushState({successState: successState}, "success page", window.location.pathname); // When you click back (this refreshes the current instance)
}
} else {
isTheBackButtonPressed = false;
}
};
useEffect(() => {
window.history.pushState(null, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
return (children);
};

React-Router/Redux browser back button functionality

I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()

How and where to dispatch action when react router changes props

I may be missing something here, change in react-router params using Link will cause the props to change with the new param(s) and trigger an update cycle.
You can't dispatch an action in the update cycle but that's the only place where the parameter is available. I need to get new data when the day parameter changes in my stateless component.
The component has 2 links, previous and next day. The previous day looks like this:
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
[UPDATE]
Here is the solution so far:
The Overview component is just a function taking props and returning jsx, the following is the container that will check if date is set and if it's not it'll redirect. If date is set then it'll return Overview.
It also checks if the router day paramater changed, if it did then it'll set dispatchFetch to true.
This will then cause the render function to asynchronously dispatch the getData action.
Not sure if there would be another way to do this, I would prefer to listen to events from router and dispatch the events from there but there is no (working) way to listen to the router.
import { connect } from 'react-redux';
import React, { Component } from 'react';
import Overview from '../components/Overview';
import { getData } from '../actions';
import { selectOverview } from "../selectors";
import { Redirect } from "react-router-dom";
const defaultDate = "2018-01-02";
const mapStateToProps = (lastProps => (state, ownProps) => {
var dispatchFetch = false;
if (lastProps.match.params.day !== ownProps.match.params.day) {
lastProps.match.params.day = ownProps.match.params.day;
dispatchFetch = true;
}
return {
...selectOverview(state),
routeDay: ownProps.match.params.day,
dispatchFetch
}
})({ match: { params: {} } });
const mapDispatchToProps = {
getData
};
class RedirectWithDefault extends Component {
render() {
//I would try to listen to route change and then dispatch getData when
// routeDay changes but none of the methods worked
// https://github.com/ReactTraining/react-router/issues/3554
// onChange in FlexkidsApp.js never gets called and would probably
// result in the same problem of dispatching an event in update cycle
// browserHistory does not exist in react-router-dom
// this.props.history.listen didn't do anything when trying it in the constructor
//So how does one dispatch an event when props change in stateless components?
// can try to dispatch previous and next day instead of using Link component
// but that would not update the url in the location bar
if (this.props.dispatchFetch) {
Promise.resolve().then(() => this.props.getData(this.props.routeDay));
}
return (this.props.routeDay)//check if url has a date (mapStateToProps would set this)
? Overview(this.props)
: <Redirect//no date, redirect
to={{
pathname: "/list/" + defaultDate
}}
/>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RedirectWithDefault);
Intially I expect your Router is like,
<Route path="/sessions/:prefDay" component={MyComponent}/>
You will have to do this in getDerivedStateFromProps(),
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prefDay !== nextProps.match.params.prefDay) {
return {
prefDay: nextProps.match.params.prefDay,
};
}
// Return null to indicate no change to state.
return null;
}
Have you tried componentWillReceiveProps?
componentWillReceiveProps(props){
if(this.props.data != props.data && this.props.state.routeDay){
this.props.getData(props.state.routeDay);
}
}
To add parameter to route you need in router do this
<Route
path="/sessions/:prefDay"
....
/>
It means that now "/sessions/" rout have a parameter named "prefDay"
Example /sessions/:prefDay
and now in Link component need to add this parameter
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
You can get this value from url like this
this.props.match.params.prefDay
In RedirectWithDefaults which is a container of the stateless component I added componentDidMount (not an update cycle method):
const historyListener = (lastDay => (props,currentDay) => {
if(lastDay !== currentDay){
lastDay = currentDay;
if(!isNaN(new Date(currentDay))){
console.log("getting:",currentDay);
props.getData(currentDay);
}
}
})(undefined);
class RedirectWithDefault extends Component {
componentDidMount(history) {
this.props.getData(this.props.routeDay || defaultDate);
this.unListen = this.props.history.listen((location) => {
historyListener(
this.props,
location.pathname.split('/').slice(-1)[0]
);
});
}
componentWillUnmount(){
this.unListen();
}
Removed code from the render function. Now when it mounts it'll dispatch the action to load data and when the route changes it will dispatch it again but not in the update cycle.

How do I send custom data in react-router goBack method like push and replace methods?

I am using react-router-dom v4 and able to send custom data to the new screen using push(path, state) and replace(path, state) methods in "props.history.location"
I want to send the data back to the previous screen but could not achieve using go(n) or goBack() or goForward().
How can I solve the scenario when I need to send data back to the previous screen?
I don't think you can pass params directly when going back.
However, it is possible to accomplish this in different ways.
You can implement redux and have an action/reducer for this. (requires some boilerplate work)
Or easier, you can store your data in localstorage and read it in the page. Be careful to use async for localstorage when reading data.
These two should work.
use replace instead of push maybe.
I faced the same problem and I didn't find any official suggestion to achieve this, following is my workaround.
In the following code snippet I keep track of locations in history stack(since there is no direct way to get the history stack)
import {Switch, useHistory} from 'react-router-dom';
import { Action, History, Location } from "history";
const history = useHistory();
const locations = [];
locations.push(history.location);//since the first location is not passed to the listener we have to initialize the locations like this to keep record of the first page that user loads
history.listen((location: Location, action: Action) => {
if (Action.Push == action) {
locations.push(location);
} else if (Action.Pop == action) {
locations.pop();
} else if (Action.Replace == action) {
locations.pop();
locations.push(location);
}
})
Then I write the following back method which receives a payload that is going to be passed to the previous page.
import _ from 'lodash';
export const back = (state) => {
if (!history) {
throw 'History was not set';
}
const uniqueLocations = _.uniqBy(locations, (l: Location) => l.key);//for some reason there will be duplicates in the locations array, using uniqBy method of lodash, we can remove the duplicates
if (uniqueLocations.length >= 2) {
const targetLocation = uniqueLocations[uniqueLocations.length - 2];
history.go(-1);
history.push({
...targetLocation,
state: {
...targetLocation.state,
backState: state,
}
})
}
}
Now calling the back() method will pass the state argument of the back method to the previous screen which can be accessed like this: props.location.state.backState in the previous screen.
You could pass the information about the previous route as state whenever you change the page. To avoid code duplication, you might create a custom hook that wraps history-push. It could look something like this:
import { useHistory, useLocation } from 'react-router-dom'
export const useRouter = () => {
const history = useHistory()
const location = useLocation()
const push = (url: string, state?: any) => {
history.push(url, { ...state, from: location.pathname })
}
return { push }
}
Now, when changing the page using the custom hook:
const { push } = useRouter()
<button onClick={() => push("/profile")}> Go to profile </button>
You will be able to access the previous route:
const location = useLocation()
console.log(location.state) // { from: '/previous-path' }
Having from in state, you could navigate to the previous page and pass some info there:
push(location.state.from, { foo: 'bar' })
What you are asking is complicated because security reasons. The app should not know what happen when somebody clicks "back" - so you shouldn't be able to manipulate with that state either.
BUT
here is a workaround doing what you need:
history.replace(`/previous/url`, { state });
setTimeout(() => history.goBack());
you will need to have stored the previous url
the state will be available only for first render, so you have to store it in state
goBack() or go(-1) are used to Go back to the previous history entry. If you want to send new data to the previous route, you might as well use history.push({pathname: prevPath, state: {data: 1}});
the prevPath is what you might pass on to the nextRoute using a location state.

Update route and redux state data at same time

I have an app that has user profiles. On the user profile there are a list of friends, and when clicking on a friend it should take you to that other user profile.
Currently, when I click to navigate to the other profile (through redux-router Link) it updates the URL but does not update the profile or render the new route.
Here is a simplified code snippet, I've taken out a lot of code for simplicity sake. There are some more layers underneath but the problem happens at the top layer in my Profile Container. If I can get the userId prop to update for ProfileSections then everything will propagate through.
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) { this.props.getUser(userId) }
}
render() {
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
As you can see, what happens is that I am running the getUser action on componentWillMount, which will happen only once and is the reason the route changes but the profile data does not update.
When I change it to another lifecycle hook like componentWillUpdate to run the getUser action, I get in an endless loop of requests because it will keep updating the state and then update component.
I've also tried using the onEnter hook supplied by react-router on Route component but it doesn't fire when navigating from one profile to another since it's the same route, so that won't work.
I believe I'm thinking about this in the wrong way and am looking for some guidance on how I could handle this situation of navigating from one profile to another while the data is stored in the redux store.
So I would suggest you approach this in the following way:
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) {
// This is the initial fetch for your first user.
this.fetchUserData(userId)
}
}
componentWillReceiveProps(nextProps) {
const { userId } = this.props.params
const { userId: nextUserId } = nextProps.params
if (nextUserId && nextUserId !== userId) {
// This will refetch if the user ID changes.
this.fetchUserData(nextUserId)
}
}
fetchUserData(userId) {
this.props.getUser(userId)
}
render() {
const { user } = this.props
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
Note that I have it set up so in the componentWillMount lifecycle method, you make the request for the initial userId. The code in the componentWillReceiveProps method checks to see if a new user ID has been received (which will happen when you navigate to a different profile) and re-fetches the data if so.
You may consider using componentDidMount and componentDidUpdate instead of componentWillMount and componentWillReceiveProps respectively for the fetchUserData calls, but it could depend on your use case.

Resources