SCALE — Build Lab
開発パターン · REACT PATTERN

確認ダイアログ

CATEGORY開発パターン TYPEReact Pattern EFFORT30〜90分 DIFFICULTY
PRIMARY CODE
tsx
'use client';
import { useState, useCallback, useRef } from 'react';

// Promise ベース確認ダイアログ
type Resolver = (ok: boolean) => void;

let resolverRef: Resolver | null = null;
let setterRef: ((opt: { msg: string; danger?: boolean } | null) => void) | null = null;

export function confirm(msg: string, opt: { danger?: boolean } = {}): Promise<boolean> {
  return new Promise((resolve) => {
    resolverRef = resolve;
    setterRef?.({ msg, danger: opt.danger });
  });
}

export function ConfirmHost() {
  const [opt, setOpt] = useState<{ msg: string; danger?: boolean } | null>(null);
  setterRef = setOpt;
  if (!opt) return null;
  const close = (ok: boolean) => { resolverRef?.(ok); setOpt(null); };
  return (
    <div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4" onClick={() => close(false)}>
      <div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 max-w-md" onClick={(e) => e.stopPropagation()}>
        <p className="text-zinc-100 mb-5">{opt.msg}</p>
        <div className="flex justify-end gap-2">
          <button onClick={() => close(false)} className="px-4 py-2 rounded-lg bg-zinc-800 text-zinc-300">キャンセル</button>
          <button onClick={() => close(true)} className={`px-4 py-2 rounded-lg text-white ${opt.danger ? 'bg-rose-500' : 'bg-indigo-500'}`}>
            {opt.danger ? '削除する' : '実行する'}
          </button>
        </div>
      </div>
    </div>
  );
}

// 使い方:
// if (await confirm('本当に削除しますか?', { danger: true })) await api.delete();
前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 削除
  • 一括操作

確認ダイアログ

:LiTarget: 用途

危険な操作の前に確認するダイアログパターン。

:LiSparkle: 特徴

  • カスタムメッセージ
  • タイプ入力確認
  • Promise方式

:LiCode: コード(コピペ用)

'use client';
import { useState, useCallback, useRef } from 'react';

// Promise ベース確認ダイアログ
type Resolver = (ok: boolean) => void;

let resolverRef: Resolver | null = null;
let setterRef: ((opt: { msg: string; danger?: boolean } | null) => void) | null = null;

export function confirm(msg: string, opt: { danger?: boolean } = {}): Promise<boolean> {
  return new Promise((resolve) => {
    resolverRef = resolve;
    setterRef?.({ msg, danger: opt.danger });
  });
}

export function ConfirmHost() {
  const [opt, setOpt] = useState<{ msg: string; danger?: boolean } | null>(null);
  setterRef = setOpt;
  if (!opt) return null;
  const close = (ok: boolean) => { resolverRef?.(ok); setOpt(null); };
  return (
    <div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4" onClick={() => close(false)}>
      <div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 max-w-md" onClick={(e) => e.stopPropagation()}>
        <p className="text-zinc-100 mb-5">{opt.msg}</p>
        <div className="flex justify-end gap-2">
          <button onClick={() => close(false)} className="px-4 py-2 rounded-lg bg-zinc-800 text-zinc-300">キャンセル</button>
          <button onClick={() => close(true)} className={`px-4 py-2 rounded-lg text-white ${opt.danger ? 'bg-rose-500' : 'bg-indigo-500'}`}>
            {opt.danger ? '削除する' : '実行する'}
          </button>
        </div>
      </div>
    </div>
  );
}

// 使い方:
// if (await confirm('本当に削除しますか?', { danger: true })) await api.delete();

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加