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>
)
}
Related
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.
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)}
/>
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.
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.
I'm building a Tour component in React whose purpose is to introduce the user to the web app's interface. Parts of the "Tour" involve validating the user's actions, (e.g. if the current step involves opening a modal, once the user does so, the "Tour" should progress otherwise it should show an error if the user tries to progress by clicking 'Next').
For this I need to detect changes in the DOM, (e.g. a modal being opened or a div with a specific class appearing). I've had some ideas about wiring up an 'onNext' function that progresses the tutorial once the user interacts with certain target elements (e.g. 'Open Modal' button), but this seems like a hack, I want to govern the progression of the tour only by the actual elements present in the DOM not by listening for clicks that will result in the necessary elements showing up eventually.
One of the big constraints is avoiding MutationObservers in addition to usage of jQuery. With that said, I'm interested in hunches about how to validate the dom, how would one use pure javascript and the dom to determine the addition and removal of elements?
I think you're best served by implementing a Flux architecture to handle this. Redux is a good fit.
Create a Redux Reducer for your tour progression. The state of this reducer should be a key that corresponds to the current step of the tour that the user is within.
All components used in the tour should have access to this tour state as a prop. Use this prop to determine functionality. I.e. for your example of a dialog that must be opened, the code might look like this, within a relevant component;
openModal(){
if(this.props.tourStep == 'prompt_modal_open'){
ActionCreator.progressTourStep();
}
// code for actually opening the modal goes here
},
someOtherAction(){
if(this.props.tourStep == 'prompt_modal_open'){
//Display error message here
} else {
//normal action result here
}
}
When the user is not taking the tour, simply set tourStep in the reducer to undefined, and any tour related functionality will be turned off.
Alternately, if you want to keep your components clean and "dumb", you can put this logic directly into the action creator with the help of Redux-Thunk;
ActionCreator.openModal = function(){
return function(dispatch, getState){
var state = getState();
if(state.tourStep == 'prompt_modal_open'){
dispatch({type: 'progress_tour_step'});
}
dispatch({type: 'open_modal'});
}
}
ActionCreator.someOtherAction = function(){
return function(dispatch, getState){
var state = getState();
if(state.tourStep != undefined){
dispatch({type: 'show_error'});
} else {
dispatch({type: 'some_other_action_type'});
}
}
}