Use Prompt from React Router to block history.push - javascript

I'm using React-Router's Prompt component to block a user from switching tabs when a form is in a dirty state. Each tab has its own route and I'm using history.push to update the URL. Prompt shows up as expected when the form is dirty, but the user is still taken to the new URL when they click "cancel".
Here is the function that handles switching tabs and updating the URL.
const handleClick = (tabId, disabled, route) => {
if (disabled) {
return false;
}
setSelectedIndex(tabId);
if (route) {
history.push(route);
}
return true;
};
I'm guessing that history.push gets executed not matter what.
Every example I see of using Prompt shows it being used with Link. Is there a way to get Prompt to work with history.push?

Related

How to work with multiple tabs using react-idle-timer

I am having a react Application, where I need to display a modal with two buttons (Logout and Continue Session) when the user goes idle, It was actually working fine with the IdleTimer component from the react-idle-timer.
But If I open the Application in multiple tabs, and click on Continue session in one tab, the other tabs are not receiving that, The sample code that I have used is,
<IdleTimer
timeout={1000}
onIdle={// Opens a modal with two buttons Logout and Continue Session}
></IdleTimer>
Is it possible to use the crossTab prop? but I am not sure how to implement them.
How can I achieve my requirement to work with multiple tabs so that if I click on continue session all the tabs need to close the modal.
The version I am using is - react-idle-timer (4.6.4)
Could someone please help with this to achieve my requirement? Thanks in advance!!
You can use the Cross Tab Messaging feature from the documentation:
// Action dispatcher (redux)
const dispatch = useDispatch()
// Message handler
const onMessage = data => {
switch (data.action) {
case 'LOGOUT_USER':
dispatch(logoutAction())
break
// More actions
default:
// no op
}
}
// IdleTimer instance
const { message } = useIdleTimer({ onMessage })
// Logout button click
const onLogoutClick = () => {
// Tell all tabs to log the user out.
// Passing true as a second parameter will
// also emit the event in this tab.
message({ action: 'LOGOUT_USER' }, true)
}
See the documentation for v5, here.

Next.js behavior on back button pressed

I have a page I am trying to fix in order to keep scroll position when user presses back button (browser). Let's say I have a component called list, where I show the user some products. To see all the products the user can scroll down the list component. When the user clicks on some product, the application redirects the user to the detail component. Then when the user tries to go back to the list, hits the back button of the browser, the list component gets rendered and it seems like it scrolls to top automatically.
As far as I know, pressing the back button of the browser triggers a window.history.back() action, nothing else happens.
For a solution, I have implemented a variable in the context of my application that saves the scrollY value and then, in the componentWillMount (or useEffect) of the component I am trying to render (list component), I set the scroll position to the value set in the context.
Details of my solution are here, as I have based my entire code in this stack overflow's post:
How to change scroll behavior while going back in next js?
I have checked the value using some logs and the scroll position is saved correctly in the context, however, as I am using a window event listener, it sets the value to zero just after the list component is rendered.
In my code I am not using any kind of scroll configuration, so I was wondering if that behavior is some sort of default for either Next.js or react. It happens when the user hits the back button of the browser, but I am a newbie to next and I don't know if I am missing something or what, I don't even know if this issue has something to do with React or Next.js itself.
This gist may be of assistance as it includes a custom hook to manage scroll position: https://gist.github.com/claus/992a5596d6532ac91b24abe24e10ae81
import { useEffect } from 'react';
import Router from 'next/router';
function saveScrollPos(url) {
const scrollPos = { x: window.scrollX, y: window.scrollY };
sessionStorage.setItem(url, JSON.stringify(scrollPos));
}
function restoreScrollPos(url) {
const scrollPos = JSON.parse(sessionStorage.getItem(url));
if (scrollPos) {
window.scrollTo(scrollPos.x, scrollPos.y);
}
}
export default function useScrollRestoration(router) {
useEffect(() => {
if ('scrollRestoration' in window.history) {
let shouldScrollRestore = false;
window.history.scrollRestoration = 'manual';
restoreScrollPos(router.asPath);
const onBeforeUnload = event => {
saveScrollPos(router.asPath);
delete event['returnValue'];
};
const onRouteChangeStart = () => {
saveScrollPos(router.asPath);
};
const onRouteChangeComplete = url => {
if (shouldScrollRestore) {
shouldScrollRestore = false;
restoreScrollPos(url);
}
};
window.addEventListener('beforeunload', onBeforeUnload);
Router.events.on('routeChangeStart', onRouteChangeStart);
Router.events.on('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => {
shouldScrollRestore = true;
return true;
});
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
Router.events.off('routeChangeStart', onRouteChangeStart);
Router.events.off('routeChangeComplete', onRouteChangeComplete);
Router.beforePopState(() => true);
};
}
}, [router]);
}
Looking at your url, using shallow routing could solve the problem. Where the URL will get updated. And the page won't get replaced, only the state of the route is changed. So you can change your logic according to that.
A good example is in the official documentation:
https://nextjs.org/docs/routing/shallow-routing
And you might use display: 'hidden' to hide and show your components conditionally according to your state!
It's a way around but it could be even more useful depending on your exact situation !
After looking for another solution that does not use the window.scroll and similar methods, I have found a solution.
1st solution (worked, but for me that I have an infinite list that is loaded via API call, sometimes the window.scroll method wasn't accurate): I take the window.scrollY value and set it in the session storage, I did this before leaving the list page, so in the details page, if user hits the back button, at the moment the page is loading, I get the Y value from session storage and use the window.scroll method to force the page to scroll to the previously configured value.
As I mentioned earlier, this worked, but in my case, I have a list that is populated from an async API call, so sometimes the page loaded without all the images and the scroll was already configured, then the images and data were loaded and the user ended up seeing some other place in the page rather than the desire position.
2nd solution: In my case we are talking about a e commerce app, so I found this solution useful as it focuses in a particular item with its corresponding ID instead of the Y coord of the window. Scroll Restoration in e commerce app

How to make a modal toggle only when navigating from a specific page in React Native?

I have two screens in my app called "ReadyScreen" and "RunningScreen."
The user can access ReadyScreen on one of two ways:
1: When they are getting ready to start a run (when they click the start run button)
2: When they finish the run, they will be navigated from RunningScreen back to the ReadyScreen.
When the user finishes a run, i want a modal to toggle showing them:
How Many Steps They Ran, Average HR, etc.
but I only want it to toggle when they are navigating from "RunningScreen."
Would this require an "if" statement basically stating if they navigated from RunningScreen the modal will toggle, else, it will not? Or is there more nuance to this?
i would send a route param in RunningScreen:
navigation.navigate('ReadyScreen', {
didComeFromRunningScreen: true
})
then in RunningScreen get the variable:
const RunningScreen = ({ route }) => {
const { didComeFromRunningScreen } = route.params
useEffect(() => {
if(didComeFromRunningScreen){
//show modal
}
}, [])
}
i believe this should work.

How to intercept browser navigation and prompt user in React (including Browser Navigation Away and Tab close)?

Here's my scenario:
I have an editor in a React component that might have unsaved data.
The only way to know this, for now, is to call a function that returns a boolean.
So I have to determine if the component has unsaved data when user is trying to 1. navigate away 2. pressed any browser navigation buttons 3. Closed the tab or window.
I'm using React Router 5
I have tried using Prompt like this:
<Prompt
when={this.getIsEditorDirty()}
message={location => `Are you sure you want to quit editing ?`}
/>
But when only takes the value as state as expected not through a function. So it doesn't behave correctly.
Since if the editor is dirty or not can be only be known through a function (assuming getIsEditorDirty()), I don't seem to find any proper way to intercept any kind of user navigation.
What could be the possible solution or way around here ?
I don't know if it's still the case in Router 5, but in 4 I do it by returning true from function passed as "message" prop if the editor is "clean".
getBlockMessage = () => {
return this.isChanged() ? LEAVE_MSG : true;
}
// ...
<Prompt message={this.getBlockMessage}/>
Here's the full implementation of the solution in addition to what #g_ain has posted. This should clarify the solution even further.
This also includes the tricky part where the usage of when in Prompt is needed to prevent an endless navigation loop. (If you are using a custom Confirmation Modal)
//Assuming this is the callback from
//your custom prompt modal's YES button
onConfirmExitClick() {
history.push(this.state.nextLocation);
}
shouldShowExitConfirmModal(nextLocation) {
if (this.getIsEditorDirty()) {
this.setState({
nextLocation,
showExitConfirmModal: true
});
return false;
} else {
return true;
}
}
//Inside render()
//Notice the when prop, it doesn't fire when the custom modal is showing
<Prompt
when={!this.state.showExitConfirmModal}
message={nextLocation => this.shouldShowExitConfirmModal(nextLocation)}
/>

React & MobX - Confirmation dialog when a user navigates away from the existing page

I have something that looks like this:
import React from 'react';
import PropTypes from 'prop-types';
import { Prompt } from 'react-router-dom';
const ConfirmationDialog = (props) => {
if (props.navigatingAway) {
window.onbeforeunload = () => true;
} else {
window.onbeforeunload = null;
}
return (
<Prompt
when={props.navigatingAway}
message="Are you sure?"
/>
);
};
ConfirmationDialog.propTypes = {
navigatingAway: PropTypes.bool.isRequired,
};
export default ConfirmationDialog;
I'm trying to figure out the best way to extend this so that navigatingAway actually does something. I don't understand what criteria to use for it, necessarily, just that it should trigger the confirmation window when:
a user changes the URL and attempts to navigate away
a user clicks on a link
a user refreshes the browser
What would be the best way to check for URL changes for when?
You don't need to come up with a way to 'detect' when one of your scenarios is occurring.
a user changes the URL and attempts to navigate away
a user refreshes the browser
These are already handled by virtue of assigning a callback to onbeforeunload.
a user clicks on a link
This is already handled by virtue of Prompt being rendered, if you're handling navigation with react-router.
props.navigatingAway, then, would be better named props.shouldPreventNavigation or something along those lines, because it should signal IF you should prevent navigating, not whether you ARE navigating.
For example, if you ALWAYS want a prompt to appear before navigation while ConfirmationDialog is mounted, then props.shouldPreventNavigation should just always be true, and you're done. A common use case would be to set it to true if there is unsaved data in a form.
From the docs for Prompt:
Instead of conditionally rendering a <Prompt> behind a guard, you can always render it but pass when={true} or when={false} to prevent or allow navigation accordingly.
To illustrate this, the following two snippets are functionally equivalent, apart from performance and such:
render() {
return (
<Prompt
when={this.props.navigatingAway}
message="Are you sure?"
/>
)
}
render() {
if (this.props.navigatingAway) {
return (
<Prompt
when={true}
message="Are you sure?"
/>
)
}
return null;
}
If Prompt isn't working properly out of the box when when={true}, then it could be that your routing isn't being properly managed by react-router.
As a side note, make sure you consider what happens with window.onbeforeunload if, for example, your ConfirmationDialog unmounts while it has a callback assigned. Use the appropriate lifecycle methods to manage this, or things are gonna get weird when you're testing this.

Categories