How to run a function when user clicks the back button, in React.js? - reactjs

I looked around and tried to find a solution with React router.
With V5 you can use <Promt />.
I tried also to find a vanilla JavaScript solution, but nothing worked for me.
I use React router v6 and histroy is replaced with const navigate = useNavigation() which doesn't have a .listen attribute.
Further v6 doesn't have a <Promt /> component.
Nevertheless, at the end I used useEffect clear function. But this works for all changes of component. Also when going forward.
According to the react.js docs, "React performs the cleanup when the component unmounts."
useEffect(() => {
// If user clicks the back button run function
return resetValues();;
})

Currently the Prompt component (and usePrompt and useBlocker) isn't supported in react-router-dom#6 but the maintainers appear to have every intention reintroducing it in the future.
If you are simply wanting to run a function when a back navigation (POP action) occurs then a possible solution is to create a custom hook for it using the exported NavigationContext.
Example:
import { UNSAFE_NavigationContext } from "react-router-dom";
const useBackListener = (callback) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
const listener = ({ location, action }) => {
console.log("listener", { location, action });
if (action === "POP") {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Usage:
useBackListener(({ location }) =>
console.log("Navigated Back", { location })
);
If using the UNSAFE_NavigationContext context is something you'd prefer to avoid then the alternative is to create a custom route that can use a custom history object (i.e. from createBrowserHistory) and use the normal history.listen. See my answer here for details.

Related

How to handle the browser's "back" button in React?

I have "react-dom-router v6.3.0" (strictly!) now and I couldn't understand how to handle browser's "back" button. For example I need to catch the event, so I could open warning modal that user leaves the page after clicking back. At least give me a direction, please.
I'm using Typescript 4.4.2.
The useBackListener:
import { useEffect, useContext } from "react";
import { NavigationType, UNSAFE_NavigationContext } from "react-router-dom";
import { History, Update } from "history";
const useBackListener = (callback: (...args: any) => void) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator as History;
useEffect(() => {
const listener = ({ location, action }: Update) => {
console.log("listener", { location, action });
if (action === NavigationType.Pop) {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Then usage:
useBackListener(({ location }) => {
if (isDirty) {
setOpenWarning(true)
} else navigate("go back")
})
How to open modal if form is dirty without redirecting after clicking browser's "back" button ? Also, is it possible to avoid #ts-ignore?
You can create your own Custom Router with history object which will help you to listen actions such as "POP" for react-router-dom v6.
To create custom route you may want to follow these steps: https://stackoverflow.com/a/70646548/13943685
This how React Router specific history object comes into play. It provides a way to "listen for URL" changes whether the history action is push, pop, or replace
let history = createBrowserHistory();
history.listen(({ location, action }) => {
// this is called whenever new locations come in
// the action is POP, PUSH, or REPLACE
});
OR you can also use
window.addEventListener("popstate", () => {
// URL changed!
});
But that only fires when the user clicks the back or forward buttons. There is no event for when the programmer called window.history.pushState or window.history.replaceState.

React Router how to update the route twice in one render cycle

I think it is an obvious limitation in React & React Router, but I'm asking maybe someone will have another idea, or someone else that will come here and will understand it impossible.
I have two custom hooks to do some logic related to the navigation. In the code below I tried to remove all the irrelevant code, but you can see the original code here.
Custom Hook to control the page:
const usePageNavigate = () => {
const history = useHistory()
const navigate = useCallback((newPage) => history.push(newPage), [history])
return navigate
}
Custom Hook to constol the QueryString:
const useMoreInfo = () => {
const { pathname } = useLocation()
const history = useHistory()
const setQuery = useCallback((value) => history.replace(pathname + `?myData=${value}`), [history, pathname])
return setQuery
}
Now you can see the *QueryString hook is used the location to concatenate the query string to the current path.
The problem is that running these two in the same render cycle (for example, in useEffect or in act in the test) causes that only the latest one will be the end result of the router.
const NavigateTo = ({ page, user }) => {
const toPage = usePageNavigate()
const withDate = useMoreInfo()
useEffect(() => {
toPage(page)
withDate(user)
}, [])
return <div></div>
}
What can I do (except creating another hook to do both changes as one)?
Should/Can I implement a kind of "events bus" to collect all the history changes and commit them at the end of the loop?
Any other ideas?

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

Resources