22
Never ask for consent ever again
Dangerous operations often require a user input. For example, your UI might have a delete button that will destroy some resource, perform an irreversible operation or launch a missile.
In such cases, it is preferable to prompt the application user for consent before performing the dangerous operation.
This article implements a React abstraction that will prevent you from asking for consent ever again.
In your view:
- Render a modal component that is controlled by a boolean state. This state controls whether the modal is opened or not.
- The modal component either calls a callback when the user clicks "Confirm" or implements the logic to perform the operation that requires confirmation.
In React pseudo-code:
const [opened, setOpened] = useState(false);
const launch = useLaunchMissile();
return (
<div>
<button onClick={() => setOpened(true)}>Launch missile</button>
<ConfirmationModal
opened={opened}
onConfirm={launch}
onClose={() => setOpened(false)}
/>
</div>
)
The problem with this approach is that you have to add code in your UI for each user confirmation.
It is possible to create an abstraction around prompts, and to inject a method that calls this abstraction.
- First, we will create an abstraction around our prompts. In React, we can create this with a context and a custom hook:
// `./context/DialogProvider`
import {useState, createContext, useMemo} from 'react';
export const DialogContext = createContext({});
export function DialogProvider({ children }) {
const [Dialog, setDialog] = useState(); // Dialog has type ReactNode
const context = useMemo(() => ({ setDialog }), []);
return (
<>
<DialogContext.Provider value={context}>{children}</DialogContext.Provider>
{Dialog}
</>
);
}
// `./hooks/use-dialog.js`
import { useContext, useCallback, useEffect } from 'react';
import { DialogContext } from '../context/DialogProvider';
export function useDialog() {
const { setDialog } = useContext(DialogContext);
const close = useCallback(() => setDialog && setDialog(null), [setDialog]);
const add = useCallback((node) => setDialog && setDialog(node), [setDialog]);
useEffect(() => {
return close;
}, [close]);
return {
add,
close,
};
}
The code above allows us to render a dialog/modal/prompt component from anywhere in the code.
- Second, we will use the abstraction above to render our prompt from a React hook:
// ./hooks/use-user-consent.jsx
import { useDialog } from './use-dialog';
import { ConfirmationDialog } from '../components/ConfirmationDialog';
export function useUserConsent() {
const { add, close } = useDialog();
return () =>
new Promise((resolve) => {
const onClose = (accepted) => {
close();
resolve(accepted);
};
add(
<ConfirmationDialog
onAccept={() => onClose(true)}
onDismiss={() => onClose(false)}
/>,
);
});
}
The code above returns a function that returns a Promise. This promise will resolve to true
if the user clicked confirm
, and resolve to false otherwise. If you wish to test the code, here is a dumb implementation of the ConfirmationDialog component:
// `./components/ConfirmationDialog.jsx`
export function ConfirmationDialog({ onDismiss, onAccept }) {
return (
<div>
<div>Are you sure?</div>
<button onClick={onAccept}>OK</button>
<button onClick={onDismiss}>Close</button>
</div>
)
}
- Ask for consent with our abstraction:
// App.js
import { DialogProvider } from './context/DialogProvider'
import { ConsentTest } from './components/ConsentTest'
function App() {
return (
<DialogProvider>
<ConsentTest />
</DialogProvider>
);
}
export default App;
// `./components/components/ConsentTest.jsx
import { useCallback } from "react";
import { useUserConsent } from "../hooks/use-user-consent";
export function ConsentTest() {
const hasApproval = useUserConsent();
const callback = useCallback(async () => {
const userConfirmed = await hasApproval();
alert(userConfirmed);
}, [hasApproval]);
return <button onClick={callback}>Test</button>
}
We have just seen a way of abstracting asking for user consent.
This can easily be extended by adding properties to the "hasApproval
" method to have a configurable prompt message.
22