window.confirm()
みたいなカスタムフックを作るお話です。
今回の使用技術
- 言語
- TypeScript
- フレームワーク
- Next.js( React.js 製 )
とある休日
娘「ねぇ、パパ?」
ワイ「なんや、娘ちゃん?」
娘「ユーザーさんのタイミングでPromise
をresolve()
することはできないの?」
ワイ「プロミス・・・?」
ワイ「なんや、借金の話かいな」
ワイ「娘ちゃんも、大人みたいなことを考えるようになったなぁ」
ワイ「もう小学一年生やもんな」
ワイ「それで、なんやっけ?」
娘「だから、ユーザーさんのタイミングでPromise
をresolve()
することはできないの?」
ワイ「プロミス・・・つまり消費者金融で借りたお金を」
ワイ「借りたユーザー側のタイミングでresolve
・・・つまり解決したい訳やな」
ワイ「その場合、リボ払いがオススメや」
ワイ「お金がない月は少しだけ返済して」
ワイ「お金のあるタイミングで多めに返済すればええ訳や」
よめ太郎「おい」
よめ太郎「6歳児に何を教えとんねん」
よめ太郎「しばくぞ」
娘「そうだよ、パパ」
娘「私が言っているのは、JavaScript
のPromise
だよ」
よめ太郎「せや」
よめ太郎「6歳児が知りたいPromise
いうたら、JavaScript
のことに決まっとるやろ」
ワイ「おお、せやったな」
ユーザーのタイミングでresolve()
とは?
ワイ「ほな、もうちょっと詳しく教えてもらえる?」
娘「えっとね」
娘「Promise
といえば」
// Promiseオブジェクトを生成する。
// 生成する時に渡す関数は「1秒後にresolve()する」という関数。
const promise = new Promise((resolve) => setTimeout(resolve, 1000))
// 1秒後に実行したい処理内容を、then()メソッドに渡す。
promise.then(() => console.log("1秒経ちました!"))
娘「↑こんな感じで使えるじゃん?」
ワイ「おお、せやな」
ワイ「つまり」
-
new Promise()
することでPromise
オブジェクトを生成する- その際に「1秒後に
resolve()
する」という関数を渡しておく
- その際に「1秒後に
- その後の行で「1秒後に処理したい内容」を
then()
メソッドに渡す
ワイ「↑こんな感じやな」
娘「そうそう」
娘「そうすることで、1秒間待機した後に、次の処理を実行できるでしょ?」
ワイ「せやな」
ワイ「非同期処理をいい感じに扱えるわけやな」
娘「あと、ほかにも」
- APIからデータが返ってくるまで待機する
- データが返って来たら、それを使って次の処理をする
娘「なんてこともできるじゃない?」
ワイ「せやな」
娘「でも、そうじゃなくて」
- ユーザーがボタンを押したら、
resolve()
される
娘「↑こういうことはできないの?」
ワイ「うーん、つまり」
- 1秒経つまで待機する
- APIからデータが返ってくるまで待機する
ワイ「↑こういう感じじゃなくて」
- ユーザーがボタンを押すまで待機する
ワイ「↑こういうことをしたい訳か」
ワイ「うーん、考えたことなかったな」
ワイ「ていうか、それ何かの役に立つん?」
オリジナルデザインの確認ダイアログを作りたい
娘「実はね」
娘「確認ダイアログを表示したいの」
ワイ「ほうほう」
娘「でもね」
娘「↑こういう、window.confirm()
で表示できるような」
娘「ブラウザのデフォルトのやつじゃなくて」
娘「オリジナルデザインのやつを表示したいの」
ワイ「ほうほう」
娘「それで」
- ユーザーが「登録」ボタンをクリックする
- 「登録してよろしいですか?」という確認ダイアログが表示される
- ユーザーが「OK」か「キャンセル」のボタンを押すまで、処理は待機される
- ボタンが押されたら、次の処理が実行される
娘「↑こんなことをしたいの」
ワイ「なるほどな」
ワイ「要はwindow.confirm()
みたいなことがしたいわけやな」
娘「そうそう、window.confirm()
の独自デザイン版って感じだね」
娘「だから」
- ユーザーがボタンを押したら、
resolve()
される
娘「↑こうする必要があると思うの」
ワイ「むむぅ・・・」
とりあえず、呼び出す側のコードを書いてみる
ワイ「娘ちゃんが言うてるような機能を持つ関数を作ってみようと思うんやけど」
ワイ「ちょっと、その関数の実装イメージが湧かへんから」
ワイ「とりあえず、その関数を呼び出す側のコードを書いてみるわ」
// 登録ボタンを押すと実行される関数
const onSubmit = async () => {
// 確認ダイアログを表示し、
// ユーザーが「OK」か「キャンセル」を押すまで待機する。
const bool = await myConfirm()
// ユーザーが「OK」か「キャンセル」を押すと、以下が実行される。
if (bool) {
window.alert("ここで登録処理を実行。")
} else {
window.alert("キャンセルなので何もしない。")
}
}
ワイ「↑こんな感じやな?」
娘「そうそう、まさにこんな感じ」
ワイ「ほな、このmyConfirm
関数を作らなアカンってことか」
娘「そうだね」
娘「あと、UIの方は」
<section>
<h1>登録ページ</h1>
<p>登録したい方は、登録ボタンを押してください。</p>
<button onClick={onSubmit}>登録</button>
{/* 確認ダイアログ */}
<Dialog open={isOpen}>
<p>登録してよろしいですか?</p>
<div>
<button onClick={cancel}>キャンセル</button>
<button onClick={ok}>OK</button>
</div>
</Dialog>
</section>
画面イメージ
↓登録画面
↓確認ダイアログ
娘「↑こんなイメージなの」
ワイ「なるほどな」
- ユーザーが「登録」ボタンを押す。
-
isOpen
変数がtrue
になることで、確認ダイアログが表示される
ワイ「↑こんな感じやな」
ワイ「ほな、isOpen
っていう変数が必要やな」
ワイ「ダイアログの開閉状態を表す変数やな」
娘「そうだね」
娘「あとは」
- 確認ダイアログ内の
- 「OK」ボタンを押すと
ok
関数が実行される - 「キャンセル」ボタンを押すと
cancel
関数が実行される
- 「OK」ボタンを押すと
娘「↑こんな機能も必要だから」
娘「ok
関数とcancel
関数も用意しないとだね」
ワイ「せやな」
ワイ「ってことは」
const {
myConfirm,
isOpen,
ok,
cancel,
} = useConfirm()
ワイ「↑こんな感じで、useConfirm
っていうカスタムフックを作って」
-
myConfirm
関数- 確認ダイアログを起動するための関数
-
isOpen
変数- ダイアログの開閉状態を表す真偽値
-
ok
関数- 「OK」ボタンが押された時に実行する関数
-
cancel
関数- 「キャンセル」ボタンが押された時に実行する関数
ワイ「↑これらを、まとめて作ってくれるようにしておけば」
ワイ「色んな画面で使い回せて便利そうやな」
娘「そうだね!」
カスタムフックを書いてみる
ワイ「とりあえず、概要だけ書いてみるで」
export const useConfirm = () => {
// 確認ダイアログを起動するための関数
const myConfirm = () => {}
return {
myConfirm, // 確認ダイアログを起動するための関数
isOpen, // ダイアログの開閉状態
ok: () => {}, // 「OK」ボタン用の関数
cancel: () => {}, // 「キャンセル」ボタン用の関数
}
}
ワイ「↑こんな感じやな」
ワイ「useConfirm
を実行すれば、返り値として必要な関数と変数を受け取れるイメージや」
娘「そうだね」
娘「それで、isOpen
はダイアログの開閉状態を表すから」
娘「状態管理が必要だね」
娘「だから・・・」
// このカスタムフックで管理する状態を表す型
type State = {
// ダイアログの開閉状態
isOpen: boolean
}
// 状態の初期値
const initialState: State = {
// ダイアログは閉じている
isOpen: false,
}
娘「↑こんなステート(状態)を定義しておくね」
ワイ「なるほどな」
ワイ「ほんで、確認ダイアログを起動するためのmyConfirm
関数を実行したら」
ワイ「isOpen
がtrue
になるようにせんとアカンから・・・」
export const useConfirm = () => {
+ // 状態を管理
+ const [state, setState] = useState<State>(initialState)
// 確認ダイアログを起動するための関数
const myConfirm = () => {
+ // 新しい状態を定義。
+ const newState: State = {
+ isOpen: true, // ダイアログを開いた状態に。
+ }
+
+ // 状態を更新。
+ setState(newState)
}
return {
myConfirm, // 確認ダイアログを起動するための関数
+ isOpen: state.isOpen, // ダイアログの開閉状態
ok: () => {}, // 「OK」ボタン用の関数
cancel: () => {}, // 「キャンセル」ボタン用の関数
}
}
ワイ「↑こうやな」
ワイ「これで、ダイアログの開閉状態を表すisOpen
がtrue
になるから」
ワイ「確認ダイアログが開くはずや」
娘「そうだね」
ワイ「そういえば、Promise
も書くって話だったけど、どこに書けばいいんや?」
娘「myConfirm
関数の中じゃないかな?」
娘「myConfirm
を実行したときに」
- 確認ダイアログを開く
- それと同時に
- ユーザーがボタンを押したら
resolve()
されるようにする
- ユーザーがボタンを押したら
娘「↑こういう非同期処理をしたいわけだから」
ワイ「せやな」
ワイ「ほな・・・」
// 確認ダイアログを起動するための関数
const myConfirm = () => {
+ const promise: Promise<boolean> =
+ new Promise((resolve) => {
// 新しい状態を定義。
const newState: State = {
isOpen: true, // ダイアログを開いた状態に。
}
// 状態を更新。
setState(newState)
+ })
+ return promise
}
ワイ「↑こんな感じやな」
ワイ「これでmyConfirm
関数は、ダイアログを開きつつ、Promise
オブジェクトを生成して返す」
ワイ「そんな関数になったわけやな」
娘「そうだね・・・あっ」
娘「このときに、resolve
もステートに保存しておけば良いんじゃない?」
ワイ「と言いますと?」
娘「えっとね」
// 確認ダイアログを起動するための関数
const myConfirm = () => {
const promise: Promise<boolean> =
new Promise((resolve) => {
// 新しい状態を定義。
const newState: State = {
isOpen: true, // ダイアログを開いた状態に。
+ resolve, // resolve関数をステートに保存。
}
// 状態を更新。
setState(newState)
})
return promise
}
娘「↑こうだね」
ワイ「へぇ、resolve
関数をステートに保存するんや・・・」
娘「そう」
娘「だから、ちゃんとステートの型も」
// このカスタムフックで管理する状態を表す型
type State = {
// ダイアログの開閉状態
isOpen: boolean
+ // resolve格納用
+ resolve: (bool: boolean) => void
}
娘「↑こう変えてあげて」
娘「ステートの初期値も」
// 状態の初期値
const initialState: State = {
// ダイアログは閉じている
isOpen: false,
+ // 何もしない関数
+ resolve: () => { },
}
娘「↑こう変えておかないとね」
ワイ「ふむふむ」
ワイ「ちゃんと、型と初期値も変えてやらんとな」
ワイ「それで、resolve
関数をステートに保存すると、何かいいことあるん?」
娘「それはね」
export const useConfirm = () => {
// 状態を管理
const [state, setState] = useState<State>(initialState)
/* --- 省略 --- */
return {
myConfirm, // 確認ダイアログを起動するための関数
isOpen: state.isOpen, // ダイアログの開閉状態
+ resolve: state.resolve, // resolve関数
}
}
娘「↑こんな感じで、resolve
関数を返してあげられるようになるの」
ワイ「おお、なるほど」
ワイ「useConfirm()
がresolve
を返してあげるんやね」
ワイ「そうすることで、ページ側でresolve
関数を受け取って」
ワイ「ボタンに割り当てることができるんやな」
ワイ「つまり」
// ボタンをクリックすると Promise が resolve される
<button onClick={resolve}>OK</button>
ワイ「↑こんなイメージやな」
娘「そうそう」
ワイ「あれ?でも」
ワイ「ボタン用に返してあげたい関数は」
ワイ「resolve
やなくて」
- 「OK」ボタン用の
ok
関数 - 「キャンセル」ボタン用の
cancel
関数
ワイ「↑この2つやなかったっけ?」
娘「そういえばそうだったね」
娘「じゃあ、resolve
関数をラップしたok
関数とcalcel
関数を作って」
娘「それを返してあげることにするね」
// 「OK」ボタン用の関数
const ok = () => {
// resolveしてあげる
state.resolve(true)
// 状態は初期化。
setState(initialState)
}
// 「キャンセル」ボタン用の関数
const cancel = () => {
// resolveしてあげる
state.resolve(false)
// 状態は初期化。
setState(initialState)
}
娘「↑こうして」
return {
myConfirm, // 確認ダイアログを起動するための関数
isOpen: state.isOpen, // ダイアログの開閉状態
- resolve: state.resolve, // resolve関数
+ ok, // 「OK」ボタン用の関数
+ cancel, // 「キャンセル」ボタン用の関数
}
娘「↑こうだね」
ワイ「なるほどなぁ」
ワイ「こうやって、resolve
を戻り値の一部として」
ワイ「useConfirm
の外に返してあげれば」
ワイ「ボタンに割り当てることができるんやな」
娘「そんな感じだね」
ワイ「ほぇ〜、ところで」
ワイ「ok
関数の中のstate.resolve(true)
って部分と」
ワイ「cancel
関数の中のstate.resolve(false)
って部分で」
ワイ「真偽値を渡してるのは何なん?」
娘「それはね」
// 登録ボタンを押すと実行される関数
const onSubmit = async () => {
// 確認ダイアログを表示し、
// ユーザーが「OK」か「キャンセル」を押すまで待機する。
const bool = await myConfirm()
// ユーザーが「OK」か「キャンセル」を押すと、以下が実行される。
if (bool) {
window.alert("ここで登録処理を実行。")
} else {
window.alert("キャンセルなので何もしない。")
}
}
娘「↑ここのbool
に入ってくるの」
ワイ「ああ、そうか」
ワイ「resolve()
するときに渡す値が」
ワイ「await myConfirm()
したときの返り値になるんやね」
ワイ「そんで、OK
が押されたのか、キャンセル
が押されたのか」
ワイ「それを真偽値で判断できるんやな」
娘「そうそう」
ワイ「よっしゃ、これで」
- ユーザーがボタンを押したら、
resolve()
される
ワイ「つまり」
- ユーザーがボタンを押すまで、処理を待機する
ワイ「↑これが実現できたな!」
ワイ「使い方は・・・」
const {
myConfirm,
isOpen,
ok,
cancel,
} = useConfirm()
// 登録ボタンを押すと実行される関数
const onSubmit = async () => {
// 確認ダイアログを表示し、
// ユーザーが「OK」か「キャンセル」を押すまで待機する。
const bool = await myConfirm()
// ユーザーが「OK」か「キャンセル」を押すと、以下が実行される。
if (bool) {
window.alert("ここで登録処理を実行。")
} else {
window.alert("キャンセルなので何もしない。")
}
}
<section>
<h1>登録ページ</h1>
<p>登録したい方は、登録ボタンを押してください。</p>
<button onClick={onSubmit}>登録</button>
{/* 確認ダイアログ */}
<Dialog open={isOpen}>
<p>登録してよろしいですか?</p>
<div>
<button onClick={cancel}>キャンセル</button>
<button onClick={ok}>OK</button>
</div>
</Dialog>
</section>
ワイ「↑こんな感じや!」
娘「まさにwindow.confirm()
の独自デザイン版って感じだね!」
娘「パパ、ありがとう!」
まとめ
-
resolve
関数をどうにかして関数外に返してあげることで、ボタンに割り当てたりできる - そうすることで「ボタンがクリックされたら
resolve()
」つまり「ユーザーがボタンを押すまで処理を待機」できる -
window.confirm()
みたいに使えるカスタムフックなんかも作れる - オリジナルデザインの確認ダイアログを作りたいときなんかに使える
ワイ「↑こういうことやね!」
娘「そうだね!」
〜おしまい〜
おまけ 〜render hooks
パターン〜
娘ちゃんの更なる要望
娘「確認ダイアログって、どのページでもほとんど同じだから」
娘「このカスタムフックと一緒に共通化できないのかな」
娘「このカスタムフックが、ダイアログ自体も返してくれればいいのにな」
ワイ「ああ、useConfirm()
がダイアログも含めて返してくれたら便利そうやな」
ワイ「それは、@uhyoさんが言うてたrender hooksパターンってやつやな」
ワイ「それならワイ、できそうやわ」
ワイ「この間、後輩の@honey32くんから教えてもらったからな」
ワイ「長いから、折りたたみ表示にしておくで」
ページ側
import { NextPage } from 'next';
import { useConfirm } from '../hooks/useConfirm';
// ページのコンポーネント
const Home: NextPage = () => {
const {
myConfirm, // 確認ダイアログを起動するための関数
+ renderConfirmDialog, // 確認ダイアログ描画関数
} = useConfirm()
// 登録ボタンを押すと実行される関数
const onSubmit = async () => {
// 確認ダイアログを表示し、
// ユーザーが「OK」か「キャンセル」を押すまで待機する。
const bool = await myConfirm()
// ユーザーが「OK」か「キャンセル」を押すと、以下が実行される。
if (bool) {
window.alert("ここで登録処理を実行。")
} else {
window.alert("キャンセルなので何もしない。")
}
}
return (
<section>
<h1>登録ページ</h1>
<p>登録したい方は、登録ボタンを押してください。</p>
<button onClick={onSubmit}>登録</button>
{/* 確認ダイアログ */}
+ {renderConfirmDialog()}
</section>
)
}
export default Home
カスタムフック側
import { Dialog } from "@mui/material"
import { useState } from "react"
// このカスタムフックで管理する状態を表す型
type State = {
// ダイアログの開閉状態
isOpen: boolean
// resolve格納用
resolve: (bool: boolean) => void
}
// 状態の初期値
const initialState: State = {
// ダイアログは閉じている
isOpen: false,
// 何もしない関数
resolve: () => { },
}
export const useConfirm = () => {
// 状態を管理
const [state, setState] = useState<State>(initialState)
// 確認ダイアログを起動するための関数
const myConfirm = () => {
const promise: Promise<boolean> =
new Promise((resolve) => {
// 新しい状態を定義。
const newState: State = {
isOpen: true, // ダイアログを開いた状態に。
resolve,
}
// 状態を更新。
setState(newState)
})
return promise
}
// 「OK」ボタン用の関数
const ok = () => {
// resolveしてあげる
state.resolve(true)
// 状態は初期化。
setState(initialState)
}
// 「キャンセル」ボタン用の関数
const cancel = () => {
// resolveしてあげる
state.resolve(false)
// 状態は初期化。
setState(initialState)
}
+ // 確認ダイアログ描画関数
+ const renderConfirmDialog = () => {
+ return (
+ <Dialog open={state.isOpen}>
+ <p>登録してよろしいですか?</p>
+ <div>
+ <button onClick={cancel}>キャンセル</button>
+ <button onClick={ok}> OK </button>
+ </div>
+ </Dialog>
+ )
+ }
return {
myConfirm, // 確認ダイアログを起動するための関数
+ renderConfirmDialog, // 確認ダイアログ描画関数
}
}
ワイ「↑こうやな」
娘「けっこう使いやすそうだね」
ワイ「登録してもよろしいですか?
の文言だけは、引数で渡すようにして」
ワイ「ページごとに変えられた方がいいかもな」
娘「そうだね」
娘「それは自分でやってみる」
娘「ありがとう、パパ!」
ワイ「へへ、今回はなかなか大変やったな」
ワイ「ところで娘ちゃん」
ワイ「↑こんな地味なデザインなら」
ワイ「わざわざオリジナルで作らなくても」
ワイ「window.confirm()
でええんちゃうか・・・?」
娘「ほんとだ、独自で作る意味ないね!」
娘「じゃあ、window.confirm()
を使うことにする!」
ワイ「ファーーーーー(失神)」
〜今度こそおしまい〜
ちなみに
- カスタムフックが返す内容は
useMemo()
やuseCallback()
してから返した方が、再レンダリングを抑えられて良いと思います。
(今回は省略しました) - ダイアログコンポーネントは、どんな場所で呼び出しても
z-index
的に大丈夫なものを使用している想定です。
(Material-UI
のDialog
など) -
window.confirm()
と違うデザインの確認ダイアログを表示したい場合や、ダイアログに見出しを追加したい場合などに良さそうですね。 - ほとんど同じやり方で
window.alert()
のようなカスタムフックも作れます。