Implementando Modal de Confirmação Reutilizável com React
Tags: webdev, javascript, programming, react
Published: 2024-06-27 05:11:53

### Introdução

Modais de confirmação são componentes essenciais em muitas aplicações web, permitindo que os usuários confirmem ações críticas antes de prosseguir. Neste artigo, apresento uma solução prática para criar um modal de confirmação reutilizável em React. A abordagem utiliza um contexto para gerenciar o estado do modal e permite personalizar facilmente o título, subtítulo e textos dos botões. Esta solução foi desenvolvida com base em várias fontes e aprimorada para oferecer um fluxo de confirmação eficiente e consistente.

### Estrutura do Projeto

A estrutura básica do projeto inclui os seguintes arquivos:

- `confirmation-modal.tsx`
- `confirmation-modal.context.tsx`
- `use-confirmation-modal.ts`
- `types.ts`
- `App.tsx`
- `index.tsx`

### Componente de Modal de Confirmação

O componente `ConfirmationModal` é responsável por renderizar o modal com base nas propriedades recebidas. Utiliza o `Modal` do Ant Design para a estrutura do modal.

```jsx
import { Modal } from "antd";
import React, { useEffect } from "react";
import { useConfirmationModal } from "./use-confirmation-modal";

interface IConfirmationModalProps {
  children?: React.ReactNode;
}

const ConfirmationModal = ({ children }: IConfirmationModalProps) => {
  const { isOpen, confirmationArgs, actions } = useConfirmationModal();

  const handleCancel = () => actions.cancel?.();

  const handleConfirm = () => actions.proceed?.();

  useEffect(() => {
    if (!actions.proceed) return;

    const handleKeydown = (e: any) => {
      if (actions.proceed && isOpen && e.key === "Enter") {
        actions.proceed();
      }
    };

    window.addEventListener("keydown", handleKeydown);

    return () => window.removeEventListener("keydown", handleKeydown);
  }, [actions.proceed, isOpen]);

  return (
    <Modal
      title={confirmationArgs.title}
      okText={confirmationArgs.confirmActionText || "Confirm"}
      cancelText={confirmationArgs.cancelActionText || "Cancel"}
      onCancel={handleCancel}
      onOk={handleConfirm}
      open={isOpen}
      width={640}
    >
      <div>
        <p>{confirmationArgs.subtitle}</p>
        {children}
      </div>
    </Modal>
  );
};

export default ConfirmationModal;
```

### Explicação Detalhada do Componente

1. **Importações**: Importamos `Modal` do Ant Design e `useEffect` do React para gerenciar o ciclo de vida do componente.
2. **Props**: Definimos `IConfirmationModalProps` para aceitar children, permitindo flexibilidade na renderização do conteúdo do modal.
3. **Hook Personalizado**: Utilizamos `useConfirmationModal` para acessar o estado e as ações do contexto de confirmação.
4. **Funções de Manipulação**: `handleCancel` e `handleConfirm` chamam as funções de cancelamento e confirmação armazenadas no contexto.
5. **Efeito Colateral**: Usamos `useEffect` para adicionar e remover um ouvinte de eventos de teclado que aciona a confirmação ao pressionar a tecla Enter.
6. **Renderização do Modal**: Utilizamos o componente `Modal` do Ant Design para exibir o modal, personalizando o título, texto dos botões e conteúdo com base nos argumentos de confirmação.

### Contexto para Gerenciamento de Estado

O contexto `ConfirmationModalContext` gerencia o estado do modal, incluindo se está aberto, os argumentos de confirmação e as ações de confirmar e cancelar.

```tsx
import { createContext, ReactNode, useState } from "react";
import ConfirmationModal from "./confirmation-modal";
import { IActions, IConfirmationArgs, IContextType } from "./types";

const INITIAL_CONFIRMATION_STATE: IConfirmationArgs = {
  title: "",
  subtitle: "",
  confirmActionText: "",
  cancelActionText: "",
};

export const ConfirmationModalContext = createContext<IContextType>({
  actions: {
    proceed: null,
    cancel: null,
  },
  confirmationArgs: INITIAL_CONFIRMATION_STATE,
  isOpen: false,
  isConfirmed: () => Promise.resolve(true),
});

interface IProviderProps {
  children: ReactNode;
}

export function ConfirmationModalProvider({ children }: IProviderProps) {
  const [actions, setActions] = useState<IActions>({
    proceed: null,
    cancel: null,
  });

  const [isOpen, setIsOpen] = useState(false);

  const [confirmationArgs, setConfirmationArgs] = useState<IConfirmationArgs>(
    INITIAL_CONFIRMATION_STATE
  );

  const isConfirmed = (confirmationArgsData: Partial<IConfirmationArgs>) => {
    const promise = new Promise((resolve, reject) => {
      setActions({ proceed: resolve, cancel: reject });
      setConfirmationArgs({
        ...INITIAL_CONFIRMATION_STATE,
        ...confirmationArgsData,
      });
      setIsOpen(true);
    });

    return promise.then(
      (): boolean => {
        setIsOpen(false);
        return true;
      },
      (): boolean => {
        setIsOpen(false);
        return false;
      }
    );
  };

  return (
    <ConfirmationModalContext.Provider
      value={{ isOpen, isConfirmed, confirmationArgs, actions }}
    >
      <ConfirmationModal />
      {children}
    </ConfirmationModalContext.Provider>
  );
}
```

### Explicação Detalhada do Contexto

1. **Estado Inicial**: `INITIAL_CONFIRMATION_STATE` define o estado inicial das propriedades do modal de confirmação.
2. **Criação do Contexto**: `ConfirmationModalContext` cria o contexto que armazenará o estado e as ações do modal.
3. **Provider**: `ConfirmationModalProvider` é o componente que encapsula a lógica de estado do modal e fornece o contexto para seus filhos.
4. **Função `isConfirmed`**: Esta função aceita argumentos de confirmação, atualiza o estado do modal e retorna uma promessa que resolve ou rejeita com base na ação do usuário.

### Hook para Acesso ao Contexto

O hook `useConfirmationModal` facilita o acesso ao contexto dentro de outros componentes.

```tsx
import React from "react";
import { ConfirmationModalContext } from "./confirmation-modal.context";

export function useConfirmationModal() {
  const { isOpen, isConfirmed, confirmationArgs, actions } = React.useContext(
    ConfirmationModalContext
  );
  return { isOpen, isConfirmed, confirmationArgs, actions };
}
```

### Explicação Detalhada do Hook

1. **Importações**: Importamos `React` e `ConfirmationModalContext`.
2. **Uso do Contexto**: `useConfirmationModal` utiliza `React.useContext` para acessar e retornar o estado e as ações do contexto.

### Definição de Tipos

Definimos os tipos necessários para as propriedades e estado do modal em `types.ts`.

```tsx
import { ButtonProps } from "antd";

export interface IActions {
  proceed: null | ((value?: unknown) => void);
  cancel: null | ((value?: unknown) => void);
}

export interface IConfirmationArgs {
  title: string;
  subtitle: string;
  confirmActionText?: string;
  cancelActionText?: string;
  customCancelAction?: () => void;
  confirmButtonCustomType?: ButtonProps["type"];
  cancelButtonCustomType?: ButtonProps["type"];
}

export interface IContextType {
  actions: IActions;
  confirmationArgs: IConfirmationArgs;
  isOpen: boolean;
  isConfirmed: (
    confirmationArgsData: Partial<IConfirmationArgs>
  ) => Promise<boolean>;
}
```

### Explicação Detalhada dos Tipos

1. **IActions**: Define as funções de ação de proceder e cancelar.
2. **IConfirmationArgs**: Define os argumentos de configuração do modal, incluindo título, subtítulo e textos dos botões.
3. **IContextType**: Define o formato do contexto, incluindo ações, argumentos de confirmação, estado de visibilidade e função `isConfirmed`.

### Componente Principal

O componente `App` demonstra como usar o modal de confirmação para ações como deletar um item ou resetar uma lista.

```tsx
import { Button } from "antd";
import { useState } from "react";
import { useConfirmationModal } from "./components/confirmation-modal";
import "./styles.css";

const INITIAL_STATE = ["banana", "watermelon", "grape", "orange"];

export default function App() {
  const [list, setList] = useState(INITIAL_STATE);

  const { isConfirmed } = useConfirmationModal();

  const handleDelete = async (fruit: string) => {
    const willDelete = await isConfirmed({
      title: "Delete Fruit",
      subtitle: `Are you sure you want to delete the ${fruit}?`,
      confirmActionText: `Delete ${fruit}`,
    });

    if (!willDelete) return;

    setList((prev) => prev.filter((item) => item !== fruit));
  };

  const reset = async () => {
    const willReset = await isConfirmed({
      title: "Reset",
      subtitle: `Are you sure you want to reset the list?`,
      confirmActionText: `Reset List`,
    });

    if (!willReset) return;

    setList(INITIAL_STATE);
  };

  return (
    <div className="App">
      <h1>Confirmation Modal</h1>
      <h2>POC to confirmation modal flow!</

h2>

      <Button type="primary" onClick={reset}>
        Reset
      </Button>

      <ul>
        {list.map((fruit) => {
          return (
            <li key={fruit}>
              <Button type="default" onClick={() => handleDelete(fruit)}>
                X
              </Button>{" "}
              {fruit}
            </li>
          );
        })}
      </ul>
    </div>
  );
}
```

### Explicação Detalhada do Componente Principal

1. **Estado Inicial**: `INITIAL_STATE` define a lista inicial de itens.
2. **Estado da Lista**: Utilizamos `useState` para gerenciar a lista de itens.
3. **Uso do Hook**: Utilizamos `useConfirmationModal` para acessar a função `isConfirmed`.
4. **Função `handleDelete`**: Mostra o modal de confirmação e remove o item da lista se a confirmação for positiva.
5. **Função `reset`**: Mostra o modal de confirmação e reseta a lista se a confirmação for positiva.
6. **Renderização**: Renderiza um botão para resetar a lista e uma lista de itens com botões para deletar cada item.

### Ponto de Entrada da Aplicação

Por fim, configuramos o ponto de entrada da aplicação para usar o `ConfirmationModalProvider`.

```tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ConfirmationModalProvider } from "./components/confirmation-modal";

const rootElement = document.getElementById("root")!;
const root = ReactDOM.createRoot(rootElement);

root.render(
  <React.StrictMode>
    <ConfirmationModalProvider>
      <App />
    </ConfirmationModalProvider>
  </React.StrictMode>
);
```

### Explicação Detalhada do Ponto de Entrada

1. **Importações**: Importamos `React`, `ReactDOM`, `App` e `ConfirmationModalProvider`.
2. **Configuração do Root**: Criamos o root element e configuramos o renderizador do React.
3. **Renderização**: Envolvemos o `App` com `ConfirmationModalProvider` para fornecer o contexto a toda a aplicação.

### Benefícios desta Abordagem

- **Reutilização**: O modal de confirmação é reutilizável em toda a aplicação, evitando duplicação de código.
- **Flexibilidade**: É fácil personalizar o conteúdo e o comportamento do modal para diferentes cenários.
- **Simplicidade**: A lógica de exibição e personalização do modal é centralizada, tornando o código mais fácil de manter.
- **Consistência**: Garante uma experiência de usuário consistente ao usar o mesmo modal para diferentes ações de confirmação.

### Conclusão

Modais de confirmação são componentes importantes para a interação do usuário com ações críticas. Implementar um modal de confirmação reutilizável e personalizável, como mostrado neste artigo, melhora a consistência e a eficiência da aplicação. Esta solução aproveita o poder dos hooks do React e o contexto para gerenciar o estado do modal, oferecendo uma abordagem flexível e escalável para confirmar ações do usuário.

Espero que esta implementação ajude você a adicionar modais de confirmação eficazes em suas aplicações React. Sinta-se à vontade para adaptar e expandir esta solução conforme necessário para atender às suas necessidades específicas.

Você pode conferir o código completo e funcional no CodeSandbox através do [link](https://codesandbox.io/p/sandbox/confirmation-modal-n94nq9).


{% codesandbox n94nq9 %}