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

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.

Related

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

Redux middleware is half working but not fully cancelling action

I have some simple middleware that is kind of working but also not working
basically I have a list of users and I am trying to delete one. and then sync it up with firebase. all is well.
I'm adding some middleware so that when a user deletes one it asks if you are sure? (just using a simple alert for now). if you click cancel, it doesn't delete. if you click ok it does
so far, that is working but because of my action creators it still carrying on and deleting the user. here is some code:
// click to delete user
<button
onClick={() =>
this.props.deleteUserFromStoreThenUpdateFirebase(user)
}
>
calls this method
I think something funky is going on here, basically it shouldn't call the deletedUserFromFirebase method if I hit cancel
export const deleteUserFromStoreThenUpdateFirebase = user => {
return (dispatch, getState) => {
return dispatch(deleteUser(user)).then(() => {
return deleteUserFromFirebase(user);
})
};
};
export const deleteUser = user => {
return async dispatch => {
dispatch({ type: DELETE_USER, user: user, reqConfirm: true });
};
};
middleware:
const confirmMiddleware = store => next => action => {
if(action.reqConfirm){
if(confirm('are you sure?')){
next(action)
}
}
else {
next(action)
}
}
I also think that confirmation actions should be [only] role of the UI.
Redux store can (should?) be treated like API - request (action) > response (changed state). Are you sending request to an API and waiting to confirmation message? It would be at least strange.
But I see even more than some sense in this idea. Centralised confirmation element can have a similiar role to toasts/snackbar messages.
Problem #1: There is a strict separation between UI and store logic - you can't use middleware to show dialog. But you can change state which can used in UI to show confirmation dialog.
Problem #2: There is no simple if/then/return chain of actions, you have to buffer an action and run it after confirmation (receiving 'confirm' action) or drop it on cancel. Yes, it can be done as middleware.
IDEA:
Action dispatched as requiring confirmation is saved in buffer. You can then change action type to CONFIRM_SHOW - state will be changed, props passed, modal dialog can be shown.
On CONFIRM_OK run buffered action, next steps will be common with CONFIRM_CANCEL: clear buffer and hide modal. It could be even one value - buffer can be a modal flag (empty - hide, defined - show) dervied in mapStateToProps.
To be fully usable there should be an option to pass custom confirmation message along with reqConfirm.

React confirm modal and redux middleware

I'm new to React and Redux also. I want to remove item from the list so I dispatch an action deleteSelectedItems then I use redux middleware to catch it and show confirm. That looks like below:
Action:
export const deleteSelectedItems = () => {
return {
type: ActionTypes.ITEM.DELETE_SELECTED,
payload: {
confirm: {
message: 'Are you sure you want to delete these selected items?'
}
}
}
};
Middleware:
const confirmMiddleware = store => next => action => {
if (action.payload.confirm) {
if (confirm(action.payload.confirm.message)) {
next(action);
}
} else {
next(action);
}
};
Everything works well. Now, I don't want to use confirm() to show confirm dialog, I want to use my own ConfirmDialog component instead.
I found #Dan Abramov solution, that is great. But I am confused how to integrate those together. I want to use confirmMiddleware to dispatch an action that show modal but I don't know how to handle when user click ok or cancel on modal. How can I do that?
I managed to independently re-invent the modal management technique that Dan describes in that issue, and then pushed it a bit farther. I did a writeup of my approach at Implement a confirm modal using React & Redux.Quoting myself:
I have a central component that is responsible for displaying all currently open dialogs, in the proper layout order (ie, I can have "DialogB" on top of "DialogA", etc). The component that wants to trigger showing the dialog runs an action creator that dispatches a "SHOW_DIALOG" action, with the name of the dialog component in the payload, and arbitrary additional data attached to the action. That data is added to the store, and the dialog managing component will pass that extra data to the dialog component as props when it's rendered.
I've created some generic "picker" dialogs, like ColorPicker and IconPicker. These dialogs know nothing about the rest of my app. They can take some optional bits of data in their props (such as the initially selected color value), and are also looking for a special prop with a name like "onColorSelected". The component that requested the dialog can include the entirety of another action as part of the payload, and when the dialog has its "OK" button clicked on, that new action will be dispatched along with the "return value" of the dialog.
So, in general, my suggestion is to include a plain action object that gets passed along to the dialog, and the dialog can dispatch a copy of the action when it is closed.
Redux middleware isn't really the right place for UI, it only really works in your existing implementation because window.confirm has magical powers and can stop the thread of execution.
Instead I would recommend dispatching a separate action to open the confirm dialog, e.g. CONFIRM_DELETE_ITEMS which toggles a flag to indicate the dialog should be displayed then dispatch the DELETE_ITEMS action when the dialog confirm button has been clicked.
e.g.
function Root({ dispatch, confirmDeleteItems }) {
return (
<div>
{confirmDeleteItems ? (
<ConfirmDialog onConfirm={() => dispatch(deleteItems())} onDeny={() = dispatch(hideModal())} />
) : null}
<button onClick={() => dispatch(confirmDeleteItems())}>Delete</button>
</div>
)
}

Categories