🔄

react-callで実装する確認モーダル

2025/03/10に公開2

はじめに

Webアプリケーションにおいて、ユーザーが重要な操作(データ削除など)を行う前に確認を取るケースがあります。この確認フローをモーダル等を使って実装すると、モーダルの開閉状態をstateで管理する必要があったりと実装が複雑になりがちです。

今回は、react-callを活用することで、確認モーダルの実装を簡潔に行う方法を紹介します。

react-callとは

react-callは、Reactコンポーネントを手続き的に処理できるようにするライブラリです。
react-callに関してはこちらの記事が参考になります。
https://zenn.dev/ykicchan/articles/5415871c017b22

実装例:削除確認モーダル

1. 確認モーダルコンポーネントの作成

まず、確認モーダルコンポーネントを作成します。
今回はshadcn/uiを使って実装を行っています。

react-call-modal.tsx
'use client'

import { createCallable } from 'react-call'
import { Button } from './ui/button'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'

interface Props { message: string }
type Response = boolean

export const { Root, ...Confirm } = createCallable<Props, Response>(({ call, message }) => (
  <Dialog defaultOpen>
    <DialogContent className="sm:max-w-[425px]">
      <DialogHeader>
        <DialogTitle>削除モーダル</DialogTitle>
        <DialogDescription>
          {message}
        </DialogDescription>
      </DialogHeader>
      <DialogFooter className="flex items-center">
        <Button variant="outline" onClick={() => call.end(false)}>
          キャンセル
        </Button>
        <Button variant="destructive" onClick={() => call.end(true)}>
          削除
        </Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
))

このコンポーネントは以下の通りです

  • createCallableを使用して呼び出し可能なコンポーネントを作成
  • モーダル内のボタンクリック時にcall.end()で結果を返す

https://github.com/desko27/react-call?tab=readme-ov-file#nextjs--rsc

2. Rootコンポーネントをレイアウトへ配置

確認モーダルを使用するために、アプリケーションのルートレイアウトにRootコンポーネントを配置します。

layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="ja">
      <body>
        {children}
        <Root />
      </body>
    </html>
  )
}

3. 確認モーダルの実際の使用例

Confirm.call()の結果に基づいて削除処理を実行するか判定します

// 省略
<DropdownMenuItem onClick={async () => {
  const confirmed = await Confirm.call({ message: '本当に削除しますか?' })
  if (confirmed) {
    remove.mutate({ id })
  }
}}>
  <TrashIcon className="mr-2 size-4" />
  削除
</DropdownMenuItem>
// 省略

まとめ

react-callを使うことでモーダルの実装や、モーダル内の操作結果による処理の分岐などを簡潔に行うことができます。この他にも通知やトーストの表示等にも活用できるため、これらを実装する際はぜひreact-callの採用を検討してみてください。

chot Inc. tech blog

Discussion

junerjuner

ニュアンス Promise でいうところの withResolvers() 的な感じか……?

shiroshiro

react-callのコードを見たところ、callの中でPromiseを生成しており、

    call: (props) => {
      if (!$setStack) throw new Error('No <Root> found!')

      const key = String($nextKey++)
      let resolve: PrivateResolve<Response>
      const promise = new Promise<Response>((res) => {
        resolve = res
      })

      $setStack((prev) => [
        ...prev,
        { key, props, promise, resolve, end: createEnd(promise), ended: false },
      ])
      return promise
    },

外部からcall.end()を呼ぶことでPromiseを解決しているため、「withResolvers() 的な感じ」と言えそうだと思いました…!

  const createEnd =
    (promise: Promise<Response> | null) => (response: Response) => {
      if (!$setStack) return
      const scopedSetStack = $setStack

      scopedSetStack((prev) =>
        prev.map((call) => {
          if (promise && call.promise !== promise) return call
          call.resolve(response)
          return { ...call, ended: true }
        }),
      )

https://github.com/desko27/react-call/blob/main/react-call/src/createCallable/index.tsx