2022-02-02に投稿

Reactでasync/awaitだけで確認ダイアログを出せるようにする

今回はNext.jsですが、Reactで確認ダイアログを出す時、confirm関数的なものをawaitで呼べたら楽ちんだよなあと思うのでその実装方法を書きます。そのページに別途ダイアログ用のテンプレートは記述しない方法です。

例えば下記のような感じです。

const onClick = useCallback(async () => {
  const isConfirmed = await confirm('タイトル', 'OKですか?')
  if (!isConfirmed) {
    return
  }
  doSomething()
}, [])

ダイアログの状態管理を作成

まずはダイアログの状態管理を行います。今回は状態管理としてRecoilを使っています。下記のような useConfirmDialog.ts を作成します。

import { useCallback } from 'react'
import { atom, useRecoilState } from 'recoil'

type OpenArgs = {
  title?: string
  body: React.ReactNode
}

type DialogProps = OpenArgs & {
  resolve: (value: boolean) => void
}

const dialogPropsState = atom<DialogProps>({
  key: 'confirmDialog/dialogProps',
  default: undefined,
})

export function useConfirmDialog() {
  const [dialogProps, setDialogProps] = useRecoilState(dialogPropsState)

  const close = useCallback(
    (result: boolean) => {
      console.log({ dialogProps })
      if (dialogProps.resolve) {
        dialogProps.resolve(result)
      }

      setDialogProps(undefined)
    },
    [dialogProps]
  )

  function open({ title, body }: OpenArgs): Promise<boolean> {
    return new Promise((resolve) => {
      setDialogProps({
        title,
        body,
        resolve,
      })
    })
  }

  return { isOpen: !!dialogProps, open, close, dialogProps }
}

ダイアログが開いているかの isOpen、開くための関数 open、閉じるための close、ダイアログの設定である dialogProps を出力します。

ここで重要なのがPromiseのresolveをdialogPropsとして保存しているところです。これを保存しておき、ダイアログが終わるタイミングでそのresolveを呼び出すことで呼び出し元のawaitを終了させることができます。

ちなみにこのdialogPropsという形ではなく、タイトルや内容、resolveをそれぞれのstateにすることも考えられますが、これはだめでした。なぜかresolveが勝手にundefinedになってしまい即awaitが終了してしまいます。何か勝手に解放されてしまうのでしょうか。ということでここが一番の肝でした。

ダイアログの表示を行う

ダイアログ自体は ConfirmDialogProvider というコンポーネントを作り、アプリケーション全体を囲むレイアウトとして設置しておきます。

Next.jsであれば pages/_app.tsx に下記のような感じです。

function MyApp({ Component, pageProps }) {
  const colors = useColors()

  return (
    <RecoilRoot>
      <ConfirmDialogProvider>
        <Component {...pageProps} />
      </ConfirmDialogProvider>
    </RecoilRoot>
  )
}

ConfirmDialogProviderは下記のような感じです。Chakra UIのダイアログを使った例です。

import { useConfirmDialog } from '@/hooks/dialog/useConfirmDialog'
import { useLocale } from '@/hooks/useLocale'
import {
  AlertDialog,
  AlertDialogOverlay,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogBody,
  AlertDialogFooter,
  Button,
} from '@chakra-ui/react'
import { useRef } from 'react'

type Props = {
  children: React.ReactNode
}

export default function ConfirmDialogProvider(props: Props) {
  const { isOpen, close, dialogProps } = useConfirmDialog()
  const { title, body } = dialogProps ?? {}
  const cancelRef = useRef()

  return (
    <>
      {props.children}
      <AlertDialog
        isOpen={isOpen}
        leastDestructiveRef={cancelRef}
        onClose={() => close(false)}
      >
        <AlertDialogOverlay>
          <AlertDialogContent>
            {title && (
              <AlertDialogHeader fontSize="lg" fontWeight="bold">
                {title}
              </AlertDialogHeader>
            )}

            <AlertDialogBody mt={title ? undefined : 4}>{body}</AlertDialogBody>

            <AlertDialogFooter>
              <Button ref={cancelRef} onClick={() => close(false)}>
                Cancel
              </Button>
              <Button colorScheme="teal" onClick={() => close(true)} ml={3}>
                OK
              </Button>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialogOverlay>
      </AlertDialog>
    </>
  )
}

まとめ

これで確認が楽ちんになりました。

ツイッターでシェア
みんなに共有、忘れないようにメモ

だら@Crieit開発者

Crieitの開発者です。 Webエンジニアです(在宅)。大体10年ちょい。 記事でわかりにくいところがあればDMで質問していただくか、案件発注してください。 業務依頼、同業種の方からのコンタクトなどお気軽にご連絡ください。 業務経験有:PHP, MySQL, Laravel, React, Flutter, Vue.js, Node, RoR 趣味:Elixir, Phoenix, Nuxt, Express, GCP, AWS等色々 PHPフレームワークちいたんの作者

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント