Prompt user when url changes using react-router-dom - reactjs

Assuming I am within an edit form page that has the url route of /person/edit/123
Within my react app, I also have a website logo at the top left of my app that when clicked, returns the user to the url route /home
Using react-router-dom v6 or some other means, I need to be able to check that when a user is within an edit page and decides to click on the website logo, I need to prompt the user that changes have been made and provide some message that has a "Leave page yes/no dialog"
Unsure what approach to take inorder to accomplish the above.
I have seen other threads within SO but they are using older versions of react-router-dom.
Any guidance would be great.
UPDATE: Code used but didn't seem to work:
useEffect(() => {
window.addEventListener('beforeunload', function (e) {
var confirmationMessage =
'It looks like you have been editing something. ' +
'If you leave before saving, your changes will be lost.';
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
}, []);

This demostrate a simplified version of a complex example to manually check for dirty forms, instead of relying on unload.
Considering you have a mechanism to check for dirty forms.
e.g.
const Component = ({ text }) => {
const [ edited, setEdited ] = useState(text)
const checkDirty = () => edited !== text
return (... my form codes here...)
}
One of the solution is to create a CustomLink component. (psuedo code)
const CustomLink = React.forwardRef(({ onClick: dirty, href }, ref) => (
const beforeHref = (e) => {
e.preventDefault();
if (typeof dirty == "function") {
if (!dirty()) {
return redirect(href)
} else {
if (confirm("Should we redirect!")) {
return redirect(href);
} else {
return null
}
}
}
redirect(href);
}
// you should probably use the link component here
return <a href={href} onClick={beforeHref} {...rest} />
));
Then in the page you can create a link like
<CustomLink href="/somepage" onClick={checkDirty} />
PS: Of course in the overall pages, you must pass those props to your menu, and your logo.

Related

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

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.

React Navigation AddListener Not working Correctly

I am trying to deepLinking and catching the url when url open the page on the screen which is functionally works when my app is not working on the background. However, it doesn't work if app is working on the background.
const isFocused = useIsFocused();
useEffect(() => {
getCode();
}, [isFocused]);
const getCode = async () => {
//we will generate a button in the forget password email, link will include a url ===> mobile://auth/new-password?verification=534396
const url = await Linking.getInitialURL();
console.log('url', url);
if (url?.includes('new-password')) {
//problem, it may not work if app is still working on the background
const query = queryString.parseUrl(url);
const verifyCode = query.query.verification;
setVerificationCode(String(verifyCode));
setIsLoading(false);
} else {
Alert.alert('Something went wrong');
}
};
When I directlinked to application with the link, it console log as "url null". Is my problem on the focusing part or on the getInitialUrl function?
I was experiencing a similar issue.
In my case we used linking from NavigationContainer and it would open the same X screen left on background regardless if the deeplink data had a different value for that screen.
I fixed it by using the getId on Stack.Screen:
const getId = ({ params }) => params?.id;
<Stack.Screen name="X" component={XComponent} getId={getId} />
You can find more info on getId here https://reactnavigation.org/docs/screen/#getid.

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

History.push a link to a new tab with react router

I need to open a link to a new tab after doing some logic.
I have a button like this:
<Button
onClick={handleSubmit}
>
Preview
</Button>
with the handleSubmit() being:
const history = useHistory();
const handleSubmit = () => {
console.log("doing something");
history.push("/some-link")
}
As you can see, with my usecase it wouldn't make sense to use the Link component.
So, is there a way to push that link to a new tab, using only history.push()
React-router's history is meant to be used just for the navigation of your application and some persistance. I can't see why you need to use it to open a new tab. I think you should use the window object for this.
const handleSubmit = () => {
console.log("doing something");
const win = window.open("/some-link", "_blank");
win.focus();
}
UPDATE: Due to some comments that confirm that it is not neccesary to focus the new window we can make this solution even shorter, like:
const handleSubmit = () => {
console.log("doing something");
window.open("/some-link", "_blank");
}
If you simply want to open a new tab without any state transfer, you can just get the path using useHref hook (Docs). This is what the <Link> component internally uses. You can then open it in new tab using window.open. This would automatically add the basename etc for you.
let href = useHref(to);
window.open(href, '_blank');

Reactjs SPA - Pressing IOS Safari back button updates the url but not the component view

We have a reactjs SPA application where we want to navigate back and forth using browser back button/swipe the screen, if on Mac.
when a user clicks on a link from home page, they will be navigated to a detail page and the url looks like https://example.com/pdetail?prdt=Computer
On detail page, User has an ability to search, and when the user searches(Say Stationery), we update the url and push the url to history and the the detail page component is updated with the searched data.Like
https://example.com/pdetail?prdt=Stationery
filterSearch(searchval) {
//search related code to set variables
let newUrl = setParams(searchval, 'prdt')
this.props.history.push(`?${newUrl}`);
// dispatch an api call to get data
}
export function setParams(query = "", param) {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(param, query.trim());
return searchParams.toString();
}
Now when the user browse back using the browser back button, we expect it to go the previous data(https://example.com/pdetail?prdt=Computer) from (https://example.com/pdetail?prdt=Stationery) and Vice Versa as we move back and forth. It is working fine in chrome and IE.
But in Safari, as the user presses back button, url is changing in the browser but the component view is not.
as we navigate back, I noticed that it did not go to ComponentDidMount as we go to the same page. But it goes ComponentDidUpdate so I added my code in there.
componentDidUpdate() {
window.onpopstate = (e) => {
if(this.props.history.action == 'POP')
{
e.preventDefault();
const search = window.location.search;
const params = new URLSearchParams(search);
if(params.has('prdt') === true)
{
const selectedprdt = params.get('prdt');
this.props.fetchDetails('FETCH_PRDT_DETAILS' , selectedprdt);
}
}
}
}
Let me know how can get this back forth page navigation with consistent url and data in Safari.
It is working fine in IE and chrome.
EDIT:
I have edited my code to add an eventListener in componentDidMount and UI is working in Safari. But when I see in the logs, I noticed the event runs multiple times not just once, which is very inefficient as we are making server calls.
componentDidMount()
{
const selectedprdt = getParams(window.location, 'prdt');
if(selectedprdt !== '')
{
this.props.fetchDetails('FETCH_PRDT_DETAILS' , selectedprdt);
}
window.addEventListener('popstate', this.handleBackButton)
}
handleBackButton = (e) =>
{
if(this.props.history.action == 'POP')
{
const search = window.location.search;
const params = new URLSearchParams(search);
if(params.has('prdt') === true)
{
const selectedMarket = params.get('prdt');
this.props.fetchDetails('FETCH_PRDT_DETAILS' , selectedprdt);
}
e.preventDefault();
}
}
Please let me know how to stop eventListener to executing the calls multiple times.
Thanks,
I had to remove the same listener on component Unmount.
componentWillUnMount()
{
window.removeEventListener('popstate', this.handleBackButton)
}

Resources