Chapter 06

共通処理

Thirosue
Thirosue
2021.08.30に更新

確認ダイアログ表示、通知メッセージ表示(トースト表示)などの共通処理は可能な限り各画面で省力で利用できるようにしたいです。

  • 確認ダイアログ表示

  • 通知メッセージ表示(トースト表示)

本チャプターでは、共通処理について記載していきます。

確認ダイアログ表示

公式にあるとおり、tailwindcssでは綺麗なモーダルを表示できますが、ダイアログのメッセージなどの表示を変えるためだけに、レイアウト定義するのは煩雑です。

https://tailwindui.com/components/application-ui/overlays/modals

具体的には、テンプレートの変更なしにElementのようにコードのみでダイアログを表示できるようにしたいです。

  • Elementでの確認ダイアログ表示
this.$confirm('This will permanently delete the file. Continue?', 'Warning', {
  confirmButtonText: 'OK',
  cancelButtonText: 'Cancel',
  type: 'warning'
}).then(() => {
  this.$message({
    type: 'success',
    message: 'Delete completed'
  });
}).catch(() => {
  this.$message({
    type: 'info',
    message: 'Delete canceled'
  });          
});

カスタムコンテクスト、カスタムプロバイダー作成

React.createContextContext.Providerを作成し、プロバイダーの配下のコンポーネントでダイアログ表示を容易に利用できるようにします。

https://ja.reactjs.org/docs/context.html#reactcreatecontext
context/confirm-provider.tsx
import React, { useState, useCallback } from 'react'
import { DialogOptions } from '../data/dialog-options'
import ConfirmContext from './confirm-context'
import Confirm from '../components/template/confirm'

// 確認ダイアログのオプション、タイトル、メッセージ、ボタンのラベルなどを指定
const DEFAULT_OPTIONS: DialogOptions = {
  html: false,
  alert: false,
  title: '',
  description: '',
  confirmationText: 'Ok',
  cancellationText: 'Cancel',
}

const buildOptions = (options: DialogOptions): DialogOptions => {
  return {
    ...DEFAULT_OPTIONS,
    ...options,
  }
}

export const ConfirmProvider = ({
  children,
}: {
  children: React.ReactNode
}): JSX.Element => {
  const [options, setOptions] = useState<DialogOptions>({ ...DEFAULT_OPTIONS })
  const [resolveReject, setResolveReject] = useState([])
  const [resolve, reject] = resolveReject

  const confirm = useCallback((options: DialogOptions): Promise<void> => {
    return new Promise((resolve, reject) => {
      setOptions(buildOptions(options))
      setResolveReject([resolve, reject])
    })
  }, [])

  const handleClose = useCallback(() => {
    setResolveReject([])
  }, [])

  const handleCancel = useCallback(() => {
    reject()
    handleClose()
  }, [reject, handleClose])

  const handleConfirm = useCallback(() => {
    resolve()
    handleClose()
  }, [resolve, handleClose])

  return (
    <>
      {resolveReject.length === 2 && (
        <>
	  <!-- ダイアログコンポーネント -->
          <Confirm
            {...options}
            onSubmit={handleConfirm}
            onClose={handleClose}
            onCancel={handleCancel}
          >
	   <!-- htmlオプション指定で指定されたNodeをそのまま表示する -->
            {options.html ? (
              options.description
            ) : (
              <p className="text-sm text-gray-500 px-5 py-1">
                {options.description}
              </p>
            )}
          </Confirm>
        </>
      )}
      <!-- プロバイダーでラップ -->
      <ConfirmContext.Provider value={{ confirm }}>
        {children}
      </ConfirmContext.Provider>
    </>
  )
}

利用設定

広範囲な画面で利用するため、レイアウトコンポーネントにダイアログ表示設定を設定しておき、各画面では意識せず確認ダイアログを表示できるようにします。

components/template/dashboard-layout.tsx
import ConfirmProvider from '../../context/confirm-provider'

...(中略)...

export const DashboardLayout = ({
  children,
  title,
}: {
  children: React.ReactNode
  title: string
}): JSX.Element => {

  return (
    <>
      <QueryClientProvider client={queryClient}>
        <ConfirmProvider> <!-- 確認ダイアログ表示用プロバイダーでラップする --->
          <GlobalStateProvider>
            <Seo title={title} />
            <div className="flex h-screen bg-gray-200 font-roboto">
              <SideBar
                sidebarOpen={sidebarOpen}
                toggle={() => setSidebarOpen(false)}
              />
              <div className="flex-1 flex flex-col overflow-hidden">
                <Header toggle={() => setSidebarOpen(true)} />
                <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-200">
                  {children}
                </main>
              </div>
            </div>
          </GlobalStateProvider>
          <ReactQueryDevtools initialIsOpen={false} />
        </ConfirmProvider>
      </QueryClientProvider>
      <ToastContainer
        autoClose={3000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        position={'bottom-right'}
        pauseOnFocusLoss
        draggable
        pauseOnHover
      />
    </>
  )
}

利用

createContextで定義した方法で利用できます。
confirmメソッドにダイアログ表示オプションを渡します。

context/confirm-context.ts
export default createContext(
  {} as {
    confirm: (options: DialogOptions) => Promise<void>
  }
)

createContextを指定することで各画面で利用できますが、カスタムフックでラップし、各画面でより利用しやすいようにします。

hooks/useConfirm.ts
import { useContext } from 'react'
import ConfirmContext from '../context/confirm-context'
import { DialogOptions } from '../data/dialog-options'

const useConfirm = (): ((options: DialogOptions) => Promise<void>) => {
  const { confirm } = useContext(ConfirmContext)
  return confirm
}

export default useConfirm

各画面では、カスタムフックを利用し、引数に表示オプションを渡し、確認ダイアログ、アラートダイアログを表示します。

components/page/login-page.tsx
import useConfirm from '../../hooks/useConfirm'

...(中略)...

export const LoginPage = ({
  passwordModalOpen,
}: {
  passwordModalOpen: () => void
}): JSX.Element => {

  const confirm = useConfirm() // カスタムフックを定義

...(中略)...

  const rememberMe = async (event: any): Promise<void> => {
    if (event.target.checked) {
     // タイトル、メッセージ、アイコンなどのオプションを指定し、ダイアログを表示する
      const cancel = await confirm({
        title: '自動ログイン設定',
        icon: 'info',
        description: '自動ログインを有効にしますか?',
      })
        .then(() => {
          setCookie(null, 'rememberMe', 'true')
        })
        .catch(() => {
          return true
        })
      if (cancel) {
        event.target.checked = false
        event.preventDefault()
      }
    } else {
      const cancel = await confirm({
        title: '自動ログイン設定',
        icon: 'info',
        description: '自動ログインを無効にしますか?',
      })
        .then(() => {
          destroyCookie(null, 'rememberMe')
        })
        .catch(() => {
          return true
        })
      if (cancel) {
        event.target.checked = true
        event.preventDefault()
      }
    }
  }

通知メッセージ表示(トースト表示)

react-toastifyを用いると、tailwindcssと競合せず容易に通知メッセージを表示できます。
サンプルアプリでは、react-toastifyを用いて、トーストを表示していきます。

https://fkhadra.github.io/react-toastify/how-to-style/

css設定

_app.tsxでトースト表示用のスタイルを読み込みます。

pages/_app.tsx
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'

import 'react-toastify/dist/ReactToastify.css' // For Toast
import '../styles/global.css'

利用設定

広範囲な画面で利用するため、レイアウトコンポーネントにトースト表示設定を設定しておき、各画面では意識せずトーストを表示できるようにします。

components/template/dashboard-layout.tsx
import { ToastContainer } from 'react-toastify'

...(中略)...

export const DashboardLayout = ({
  children,
  title,
}: {
  children: React.ReactNode
  title: string
}): JSX.Element => {

  return (
    <>
      <QueryClientProvider client={queryClient}>
        <ConfirmProvider>
          <GlobalStateProvider>
            <Seo title={title} />
            <div className="flex h-screen bg-gray-200 font-roboto">
              <SideBar
                sidebarOpen={sidebarOpen}
                toggle={() => setSidebarOpen(false)}
              />
              <div className="flex-1 flex flex-col overflow-hidden">
                <Header toggle={() => setSidebarOpen(true)} />
                <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-200">
                  {children}
                </main>
              </div>
            </div>
          </GlobalStateProvider>
          <ReactQueryDevtools initialIsOpen={false} />
        </ConfirmProvider>
      </QueryClientProvider>
      <ToastContainer
        autoClose={3000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        position={'bottom-right'}
        pauseOnFocusLoss
        draggable
        pauseOnHover
      /> <!-- トースト表示のデフォルト設定を実施 --->
    </>
  )
}

利用

公式のREADME.mdに記載のとおり、簡単に利用できます。

https://github.com/fkhadra/react-toastify
components/page/confirm-code-dialog.tsx
import { toast } from 'react-toastify'

export const ConfirmCodeModal = ({
  onSubmit,
  onClose,
  onCancel,
}: {
  onSubmit: () => void
  onClose: (event: any) => void
  onCancel: (event: any) => void
}): JSX.Element => {

...(中略)...

  const doSubmit = (data: FormValues): void => {
    captains.log(data)
    const request: VerifyCodeRequest = {
      code: data.code,
    }
    mutation.mutate(request, {
      onSuccess: () => {
        onClose(null)
        onSubmit()
        toast.success('パスワードを更新しました') //トースト表示
      },
    })
  }