Recipes

Common patterns and recipes for using use-disclosable in your applications.

Confirmation Dialog

A classic yes/no confirmation dialog that returns the user's choice:

import type { DisclosableInjectedProps } from "use-disclosable";
import { Button } from "./ui/button";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from "./ui/dialog";

type ConfirmationDialogProps = {
  title: string;
  message: string;
  confirmLabel?: string;
  cancelLabel?: string;
} & DisclosableInjectedProps;

export const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
  title,
  message,
  confirmLabel = "Confirm",
  cancelLabel = "Cancel",
  isDisclosableOpen,
  closeDisclosable,
}) => {
  return (
    <Dialog open={isDisclosableOpen}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
        </DialogHeader>
        <p>{message}</p>
        <DialogFooter>
          <Button
            variant="outline"
            onClick={() => closeDisclosable({ closeReason: "cancel" })}
          >
            {cancelLabel}
          </Button>
          <Button onClick={() => closeDisclosable({ closeReason: "confirm" })}>
            {confirmLabel}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};

Usage:

const { open } = useDisclosable();

const handleDelete = async () => {
  const result = await open(ConfirmationDialog, {
    props: {
      title: "Delete Item",
      message: "Are you sure you want to delete this item?",
    },
  });

  if (result === "confirm") {
    // User confirmed - delete the item
    await deleteItem(id);
  }
};

Form Dialog

A dialog with form inputs that returns the submitted data:

import { useState } from "react";
import type { DisclosableInjectedProps } from "use-disclosable";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from "./ui/dialog";

type FormDialogProps = {
  initialValues?: { name: string; email: string };
  onSave?: (data: { name: string; email: string }) => Promise<void>;
} & DisclosableInjectedProps;

export const FormDialog: React.FC<FormDialogProps> = ({
  initialValues = { name: "", email: "" },
  onSave,
  isDisclosableOpen,
  closeDisclosable,
}) => {
  const [formData, setFormData] = useState(initialValues);

  const handleSubmit = async () => {
    if (onSave) {
      await onSave(formData);
    }
    closeDisclosable({ closeReason: "confirm" });
  };

  return (
    <Dialog open={isDisclosableOpen}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Enter Details</DialogTitle>
        </DialogHeader>
        <div className="space-y-4">
          <div>
            <label>Name</label>
            <Input
              value={formData.name}
              onChange={(e) =>
                setFormData({ ...formData, name: e.target.value })
              }
            />
          </div>
          <div>
            <label>Email</label>
            <Input
              type="email"
              value={formData.email}
              onChange={(e) =>
                setFormData({ ...formData, email: e.target.value })
              }
            />
          </div>
        </div>
        <DialogFooter>
          <Button
            variant="outline"
            onClick={() => closeDisclosable({ closeReason: "cancel" })}
          >
            Cancel
          </Button>
          <Button
            onClick={handleSubmit}
            disabled={!formData.name || !formData.email}
          >
            Save
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};

Usage:

const { open } = useDisclosable();

const handleAddUser = async () => {
  const result = await open(FormDialog, {
    props: { initialValues: { name: "", email: "" } },
  });

  if (result) {
    const formData = JSON.parse(result);
    // Submit form data
    await createUser(formData);
  }
};

Loading State with Async Operation

Show a loading state while an async operation completes:

import type { DisclosableInjectedProps } from "use-disclosable";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";

type LoadingDialogProps = {
  message?: string;
} & DisclosableInjectedProps;

export const LoadingDialog: React.FC<LoadingDialogProps> = ({
  message = "Loading...",
  isDisclosableOpen,
}) => {
  return (
    <Dialog open={isDisclosableOpen}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{message}</DialogTitle>
        </DialogHeader>
        <div className="flex justify-center py-4">
          <Spinner /> {/* Your loading spinner component */}
        </div>
      </DialogContent>
    </Dialog>
  );
};

Usage:

const { open, close } = useDisclosable();

const handleProcess = async () => {
  // Open loading dialog
  await open(LoadingDialog, { props: { message: "Processing..." } });

  try {
    await performHeavyOperation();
    close(LoadingDialog);
  } catch (error) {
    // Handle error
  }
};

Multiple Dialog Instances

Use custom identifiers to open multiple instances of the same dialog type:

const { open } = useDisclosable();

const handleOpenMultiple = () => {
  // Open first instance
  open(MyDialog, {
    identifier: "dialog-1",
    props: { title: "First Dialog" },
  });

  // Open second instance (different identifier)
  open(MyDialog, {
    identifier: "dialog-2",
    props: { title: "Second Dialog" },
  });
};

Dynamic Props Update

Update dialog props after it's already open:

const { open, setProps } = useDisclosable();

const handleShowDetails = async () => {
  await open(ItemDetailsDialog, {
    identifier: "item-details",
    props: { itemId: null },
  });

  // Later, update the props
  setProps("item-details", { itemId: 123 });
};

Alert Dialog

A simple alert that shows a message and has a single "OK" button:

import type { DisclosableInjectedProps } from "use-disclosable";
import { Button } from "./ui/button";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
} from "./ui/dialog";

type AlertDialogProps = {
  title: string;
  message: string;
} & DisclosableInjectedProps;

export const AlertDialog: React.FC<AlertDialogProps> = ({
  title,
  message,
  isDisclosableOpen,
  closeDisclosable,
}) => {
  return (
    <Dialog open={isDisclosableOpen}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription>{message}</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button onClick={() => closeDisclosable({ closeReason: "ok" })}>
            OK
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};

Usage:

const { open } = useDisclosable();

const handleShowError = async () => {
  await open(AlertDialog, {
    props: {
      title: "Error",
      message: "Something went wrong. Please try again.",
    },
  });
  // Continue after user clicks OK
};

Toast/Notification Pattern

Use closeDisclosable with a destroy delay for animated exits:

import type { DisclosableInjectedProps } from "use-disclosable";
import { Dialog, DialogContent } from "./ui/dialog";

type NotificationDialogProps = {
  message: string;
  type: "success" | "error" | "info";
} & DisclosableInjectedProps;

export const NotificationDialog: React.FC<NotificationDialogProps> = ({
  message,
  type,
  isDisclosableOpen,
  closeDisclosable,
}) => {
  const handleClose = () => {
    // Destroy after animation completes
    closeDisclosable({ destroyAfter: 300 });
  };

  return (
    <Dialog
      open={isDisclosableOpen}
      onOpenChange={(open) => !open && handleClose()}
    >
      <DialogContent>
        <div className={`notification ${type}`}>{message}</div>
      </DialogContent>
    </Dialog>
  );
};