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.

The valid approach that we want to stop using

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.

A better approach

It is possible to create an abstraction around prompts, and to inject a method that calls this abstraction.

  1. 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.

  1. 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>
  )
}
  1. 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>
}

Conclusion

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