iToverDose/Software· 12 JUNE 2026 · 16:02

Why returning typed Promises from modals fixes React apps

Managing modals with plain booleans and callbacks leads to brittle React code. Switching to typed promises simplifies flows, eliminates runtime surprises, and gives TypeScript full visibility into your modal contracts.

DEV Community4 min read0 Comments

Modern React applications rely on modals to handle sensitive user actions, collect input, and orchestrate workflows. Yet many teams treat these dialogs as simple UI toggles rather than typed async operations. The result is sprawling boolean state, tangled callback chains, and brittle contracts that break silently during refactoring. A growing number of developers are solving this by returning typed promises from modal managers instead of generic boolean flags or unlabeled any objects.

The shift transforms reactive modal patterns into predictable async flows that behave like any other API call in your codebase.

The hidden cost of boolean modal state

Traditional React modal patterns start innocently. A local boolean flag controls visibility, and an onClose callback resets it.

function ReportsPage() {
  const [isRenameOpen, setIsRenameOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsRenameOpen(true)}>
        Rename Report
      </button>
      {isRenameOpen && (
        <RenameModal 
          onClose={() => setIsRenameOpen(false)}
        />
      )}
    </>
  );
}

As the application grows, so do the requirements. Each new modal needs its own state, handler, and sometimes even input data.

const [isRenameOpen, setIsRenameOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [renameTarget, setRenameTarget] = useState<Report | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Report | null>(null);

The UI remains trivial, but the orchestration becomes unmanageable. State leaks into global stores, input data travels through context, and closing a modal requires remembering to pass the correct callback. Testing becomes fragile because state is scattered between components and effects.

Why Promises beat callbacks in modal flows

Several modal libraries already expose promise-based APIs, but many leave the result type intentionally broad. This leads to a false sense of flexibility.

// Using a popular modal library
const result = await NiceModal.show("rename-report", data);
// result is typed as `any`

With any, TypeScript offers no protection. Misspelled properties go unnoticed, extra properties sneak in, and refactoring becomes a guessing game. The modal contract is no longer verifiable at compile time.

A typed modal promise flips this model. Instead of Promise<any>, the return type reflects the exact shape of expected input and possible outcomes.

const result = await modal.open(renameReportModal, {
  reportId: report.id,
  currentName: report.name,
});

// result is now a discriminated union:
// { status: "renamed", name: string } | { status: "cancelled" }

if (result.status === "renamed") {
  await renameReport({ id: report.id, name: result.name });
}

TypeScript can now enforce that only valid fields exist, signals errors on typos, and ensures every caller handles all possible outcomes. Renaming a field in the result type breaks every usage site immediately, preventing runtime surprises.

How typed modals eliminate boilerplate

A well-designed modal system treats dialogs as typed async contracts rather than UI components. The state lives in a provider, not scattered across hooks, and opening a modal works the same way from any layer of your app.

export function App() {
  return (
    <ModalProvider>
      <ReportsPage />
    </ModalProvider>
  );
}

function DeleteButton() {
  const modal = useModalManager();

  async function handleDelete() {
    const { confirmed } = await modal.confirm({
      title: "Delete report?",
      description: "This action cannot be undone.",
      confirmText: "Delete",
      variant: "danger",
    });

    if (!confirmed) return;

    await deleteReport();
  }

  return <button onClick={handleDelete}>Delete</button>;
}

No lifted state. No callback chains. No global variables. The flow reads sequentially, just like a regular async function call. The modal manager handles scoping, cleanup, and lifecycle events behind the scenes.

Building a typed modal contract

Defining a typed modal starts with a strongly typed input and result interface. The component receives typed props and returns a specific outcome.

import { createModal, ModalComponentProps } from "@okyrychenko-dev/react-modal-manager";

interface RenameReportInput {
  reportId: string;
  currentName: string;
}

type RenameReportResult =
  | { status: "renamed"; name: string }
  | { status: "cancelled" };

function RenameReportModal({
  input,
  close,
}: ModalComponentProps<RenameReportInput, RenameReportResult>) {
  const [name, setName] = useState(input.currentName);

  return (
    <dialog open>
      <h2>Rename report</h2>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)}
      />
      <button onClick={() => close({ status: "cancelled" })}>
        Cancel
      </button>
      <button onClick={() => close({ status: "renamed", name })}>
        Rename
      </button>
    </dialog>
  );
}

export const renameReportModal = createModal<RenameReportInput, RenameReportResult>({
  component: RenameReportModal,
});

Every call site now benefits from full type inference. Input requirements, valid outcomes, and result handling are all enforced by the compiler. Changing a field in the result type triggers immediate feedback across every usage, making refactoring safer and faster.

Looking ahead: typed modals as the default

The trend toward typed async contracts reflects a broader shift in React development — replacing implicit state with explicit, verifiable contracts. Modal managers that return typed promises are no longer experimental niche tools; they are becoming the standard for scalable, maintainable React applications.

Teams adopting this pattern report fewer runtime bugs, faster refactoring cycles, and cleaner component boundaries. As TypeScript tightens its grip on modern JavaScript, modal flows that resist type checking will increasingly feel like technical debt.

The next generation of React applications won’t just open modals — they’ll invoke typed async operations with the same confidence they apply to API calls and database transactions.

AI summary

React uygulamalarında modallar için tip güvenliğini nasıl sağlarsınız? Promise<TResult> tabanlı modal yönetimiyle async operasyonları basitleştirin ve hata riskini azaltın.

Comments

00
LEAVE A COMMENT
ID #INPLF0

0 / 1200 CHARACTERS

Human check

4 + 7 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.