Want to add a modal confirmation dialog to your React + Redux application?
There are already a few good blogs and tutorials on how to do this in React + Redux. However, when I needed to add a confirmation to an app I was working on, I found that everyone who came before me seemed to focus on thunks, which are difficult to test.
Using thunks doesn't work for me because I prefer redux-saga for encapsulating workflow. I also wanted to avoid solutions that keep state in the UI components.
Thanks to redux-saga, it was easy to create a confirmation workflow that fit seamlessly into my code.
The example I'll show here is for deleting a user.
The saga for deleting a user started out as a very basic saga. As a side note, I implement all my logic this way. There is some repetition/boilerplate but overall it makes reasoning about an application very easy. Anyhow... the deleteUserSaga waited for a delete-user request, fired off a RESTful delete request to the server, and then reported success/failure. It looked like this:
userSagas.ts -- Before
export function* deleteUserSaga() { while (true) { const { payload: id } = yield take(Actions.DELETE_USER_REQUEST); try { yield call(apiClient.delete, `/api/users/${id}`); yield put(deleteUserSuccess(id)); } catch (err) { yield put(deleteUserFailure(err, id)); } }}
Redux-saga allows us to call sagas from within other sagas (read more here). And I'll be using the confirmation dialog in a few places. So it makes sense to create a new saga dedicated to handling the confirmation dialog logic.
confirmSaga.ts
import { take, put, race } from 'redux-saga/effects';import { Actions as ShellActions, showConfirmation, hideConfirmation} from '../actions/shellActions';export function* confirmSaga(confirmationMessage: string) { // Cause the dialog to be shown (reducer will put the message // in the store; the main shell UI component will receive the // message in its props and then display the dialog) yield put(showConfirmation(confirmationMessage)); // Wait for either a yes or a no. // The dialog UI component receives yes and no event handlers // in its props that dispatch these actions. const { yes } = yield race({ yes: take(ShellActions.CONFIRM_YES), no: take(ShellActions.CONFIRM_NO) }) // Tell a reducer to hide the dialog yield put(hideConfirmation()); // Returns true if the 'yes' action was received return !!yes;}
After having added the confirmation dialog, deleteUserSaga now looks like this:
userSagas.ts -- After
export function* deleteUserSaga() { while (true) { const { payload: id } = yield take(Actions.DELETE_USER_REQUEST); // Convert the user ID into a display name for the user const user = yield select(getUser, { id }); const name = user.get('displayName') || user.get('login') || ''; // Jump into the confirm saga to handle confirmation logic const message = t('USERS.DELETE_CONFIRMATION', { name }); const confirmed = yield call(confirmSaga, message); if (!confirmed) { continue; } try { yield call(apiClient.delete, `/api/users/${id}`); yield put(deleteUserSuccess(id)); } catch (err) { yield put(deleteUserFailure(err, id)); } }}
Further Reading
- redux-saga documentation
Other modal dialogs in React + Redux:
- Dan Abramov's answer on Stack Overflow for displaying a modal dialog
- react-redux-modal package via NPM
- Implement a Confirm Modal Using React & Redux
- Confirm Dialogs in React and Redux