React Video Tutorial: Confirmation Modal

In this video, I am about to show you how to manage the display of confirmation messages in a React application. We will seek to develop an approach that is reusable and practical to use.

00:00 Presentation
00:20 React approach (not reusable)
01:35 Goal
02:22 Solution 1: Global Component
09:50 Solution 2: Background

Basic approach

For the example we imagine a component that allows you to perform an action when you click on a button.

function MyComponent() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((n) => n + 1);
  };

  return (
    <>
      <p>Compteur : {count}</p>
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        <button onClick={increment}>Incrémenter</button>
      </div>
    </>
  );
}

But we want to obtain the user’s confirmation to perform the action.
The natural approach would be to use a state to know whether or not to display the confirmation message.

function MyComponent() {
  const [count, setCount] = useState(0);
  const [confirm, setConfirm] = useState(false);

  const startConfirm = () => {
    setConfirm(true);
  };

  const increment = () => {
    setCount((n) => n + 1);
    setConfirm(false);
  };

  const cancelConfirm = () => {
    setConfirm(false);
  };

  return (
    <>
      <p>Compteur : {count}</p>
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        <button onClick={startConfirm}>Incrémenter</button>
      </div>
      <ConfirmDialog
        onConfirm={increment}
        onCancel={cancelConfirm}
        open={confirm}
      />
    </>
  );
}

This approach works for a simple case but is not reusable enough and requires a lot of code. A more imperative approach would feel more natural and require little code to use.

const increment = () => {
    if (await confirm({ title: "Voulez vous vraiment incrémenter ?" })) {
        setCount((n) => n + 1)
    }
};

We could even create a higher order function withConfirm() to make it even easier to write.

Implementation

Via a global variable

This first approach makes it possible to do without a context by using a global variable which will be altered by a global component.

// Cette variable servira de "ref"
const confirmAction = {
  current: (p) => Promise.resolve(true),
};

export function confirm(props) {
  return confirmAction.current(props);
}

Our function will use the value of the “ref” which will allow us to modify without modifying the function (we will modify confirmAction.current). We are therefore going to create a global component which will precisely change the behavior of confirmAction.

export function ConfirmGlobal() {
  const [open, setOpen] = useState(false);
  const [props, setProps] = useState({});
  // On sauvegarde la fonction de résolution de la promesse
  const resolveRef = useRef((v) => {});
  // On modifie confirmAction pour le connecter à notre composant
  confirmAction.current = (props) =>
    new Promise((resolve) => {
      setProps(props);
      setOpen(true);
      resolveRef.current = resolve;
    });

  const onConfirm = () => {
    resolveRef.current(true);
    setOpen(false);
  };

  const onCancel = () => {
    resolveRef.current(false);
    setOpen(false);
  };

  return (
    <ConfirmDialog
      onConfirm={onConfirm}
      onCancel={onCancel}
      open={open}
      {...props}
    />
  );
}

For this approach to work it will be necessary to have this component <ConfirmGlobal> mounted in our application for the confirmation function to trigger the display of the modal box.

<>
  <ConfirmGlobal>
  <App/>
</>

With a context

If you want to avoid the use of a global variable (and be able to change the behavior) it is possible to use a context to save the confirmation action

type Params = Partial<
  Omit<ComponentProps<typeof ConfirmDialog>, "open" | "onConfirm" | "onCancel">
>;

const defaultFunction = (p?: Params) => Promise.resolve(true); // En l'absence de contexte, on renvoie true directement

const defaultValue = {
  confirmRef: {
    current: defaultFunction,
  },
};

const ConfirmContext = createContext(defaultValue);

// On devra entourer notre application avec ce context provider
export function ConfirmContextProvider({ children }: PropsWithChildren) {
  const confirmRef = useRef(defaultFunction);
  return (
    <ConfirmContext.Provider value={{ confirmRef }}>
      {children}
      <ConfirmDialogWithContext />
    </ConfirmContext.Provider>
  );
}

function ConfirmDialogWithContext() {
  const [open, setOpen] = useState(false);
  const [props, setProps] = useState<undefined | Params>();
  const resolveRef = useRef((v: boolean) => {});
  const { confirmRef } = useContext(ConfirmContext);
  confirmRef.current = (props) =>
    new Promise((resolve) => {
      setProps(props);
      setOpen(true);
      resolveRef.current = resolve;
    });

  const onConfirm = () => {
    resolveRef.current(true);
    setOpen(false);
  };

  const onCancel = () => {
    resolveRef.current(false);
    setOpen(false);
  };
  return (
    <ConfirmDialog
      onConfirm={onConfirm}
      onCancel={onCancel}
      open={open}
      {...props}
    />
  );
}

// Ce hook nous permettra d'accéder à la fonction confirm
export function useConfirm() {
  const { confirmRef } = useContext(ConfirmContext);
  return {
    confirm: useCallback(
      (p: Params) => {
        return confirmRef.current(p);
      },
      [confirmRef]
    ),
  };
}

We can then surround our application via the provider.

<ConfirmContextProvider>
  <App />
</ConfirmContextProvider>

Then, in our component we can use the method confirm retrieved from our hook.

function MyComponent() {
  const [count, setCount] = useState(0);
  const { confirm } = useConfirm();

  const increment = async () => {
    if (await confirm({ title: "Voulez vous vraiment incrémenter ?" })) {
      setCount((n) => n + 1);
    }
  };

  return (
    <>
      <p>Compteur : {count}</p>
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        <button onClick={increment}>Incrémenter</button>
      </div>
    </>
  );
}

And There you go !

Why not create a classic context?

We can wonder why we didn’t use a classic context where we would have saved the state of the confirmation box. The problem with such an approach is that the change of state of the confirmation would lead to a change in the value of the context. This would generate a rendering for all consumers of this context (which is counter-intuitive).