6歳娘「パパ、ユーザーのタイミングでPromiseをresolve()できないの？」

`window.confirm()`みたいなカスタムフックを作るお話です。

#### 今回の使用技術
- 言語
    - TypeScript
- フレームワーク
    - Next.js（ React.js 製 ）

# とある休日
娘「ねぇ、パパ？」

ワイ「なんや、娘ちゃん？」

娘「ユーザーさんのタイミングで`Promise`を`resolve()`することはできないの？」

ワイ「プロミス・・・？」
ワイ「なんや、**借金の話**かいな」
ワイ「娘ちゃんも、大人みたいなことを考えるようになったなぁ」
ワイ「もう小学一年生やもんな」
ワイ「それで、なんやっけ？」

娘「だから、ユーザーさんのタイミングで`Promise`を`resolve()`することはできないの？」

ワイ「プロミス・・・つまり消費者金融で借りたお金を」
ワイ「借りたユーザー側のタイミングで`resolve`・・・つまり**解決**したい訳やな」
ワイ「その場合、**リボ払い**がオススメや」
ワイ「お金がない月は少しだけ返済して」
ワイ「お金のあるタイミングで多めに返済すればええ訳や」

よめ太郎「おい」
よめ太郎「6歳児に何を教えとんねん」
よめ太郎「しばくぞ」

娘「そうだよ、パパ」
娘「私が言っているのは、`JavaScript`の`Promise`だよ」

よめ太郎「せや」
よめ太郎「6歳児が知りたい`Promise`いうたら、`JavaScript`のことに決まっとるやろ」

ワイ「おお、せやったな」

# ユーザーのタイミングで`resolve()`とは？

ワイ「ほな、もうちょっと詳しく教えてもらえる？」

娘「えっとね」
娘「`Promise`といえば」

```diff_tsx
  // Promiseオブジェクトを生成する。
  // 生成する時に渡す関数は「1秒後にresolve()する」という関数。
  const promise = new Promise((resolve) => setTimeout(resolve, 1000))

  // 1秒後に実行したい処理内容を、then()メソッドに渡す。
  promise.then(() => console.log("1秒経ちました！"))
```

娘「↑こんな感じで使えるじゃん？」

ワイ「おお、せやな」
ワイ「つまり」

- `new Promise()`することで`Promise`オブジェクトを生成する
    - その際に「1秒後に`resolve()`する」という関数を渡しておく
- その後の行で「1秒後に処理したい内容」を`then()`メソッドに渡す

ワイ「↑こんな感じやな」

娘「そうそう」
娘「そうすることで、1秒間**待機**した後に、次の処理を実行できるでしょ？」

ワイ「せやな」
ワイ「非同期処理をいい感じに扱えるわけやな」

娘「あと、ほかにも」

1. APIからデータが返ってくるまで**待機**する
1. データが返って来たら、それを使って次の処理をする

娘「なんてこともできるじゃない？」

ワイ「せやな」

娘「でも、そうじゃなくて」

- ユーザーがボタンを押したら、`resolve()`される

娘「↑こういうことはできないの？」

ワイ「うーん、つまり」

- 1秒経つまで**待機**する
- APIからデータが返ってくるまで**待機**する

ワイ「↑こういう感じじゃなくて」

- ユーザーがボタンを押すまで**待機**する

ワイ「↑こういうことをしたい訳か」
ワイ「うーん、考えたことなかったな」
ワイ「ていうか、それ何かの役に立つん？」

# オリジナルデザインの確認ダイアログを作りたい

娘「実はね」
娘「確認ダイアログを表示したいの」

ワイ「ほうほう」

娘「でもね」

![スクリーンショット 2022-06-10 9.22.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/e3459d26-9dac-53db-af30-da1c1bc0481c.png)

娘「↑こういう、`window.confirm()`で表示できるような」
娘「ブラウザのデフォルトのやつじゃなくて」
娘「オリジナルデザインのやつを表示したいの」

ワイ「ほうほう」

娘「それで」

1. ユーザーが「登録」ボタンをクリックする
1. 「登録してよろしいですか？」という確認ダイアログが表示される
1. ユーザーが「OK」か「キャンセル」のボタンを押すまで、処理は待機される
1. ボタンが押されたら、次の処理が実行される

娘「↑こんなことをしたいの」

ワイ「なるほどな」
ワイ「要は`window.confirm()`みたいなことがしたいわけやな」

娘「そうそう、`window.confirm()`の独自デザイン版って感じだね」
娘「だから」

- ユーザーがボタンを押したら、`resolve()`される

娘「↑こうする必要があると思うの」

ワイ「むむぅ・・・」

# とりあえず、呼び出す側のコードを書いてみる

ワイ「娘ちゃんが言うてるような機能を持つ**関数**を作ってみようと思うんやけど」
ワイ「ちょっと、その関数の実装イメージが湧かへんから」
ワイ「とりあえず、その関数を**呼び出す側**のコードを書いてみるわ」

```diff_tsx
  // 登録ボタンを押すと実行される関数
  const onSubmit = async () => {
    // 確認ダイアログを表示し、
    // ユーザーが「OK」か「キャンセル」を押すまで待機する。
    const bool = await myConfirm()

    // ユーザーが「OK」か「キャンセル」を押すと、以下が実行される。
    if (bool) {
      window.alert("ここで登録処理を実行。")
    } else {
      window.alert("キャンセルなので何もしない。")
    }
  }
```

ワイ「↑こんな感じやな？」

娘「そうそう、まさにこんな感じ」

ワイ「ほな、この`myConfirm`関数を作らなアカンってことか」

娘「そうだね」
娘「あと、UIの方は」

```diff_tsx:登録ページのtsx
    <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>
```

#### 画面イメージ

##### ↓登録画面
![スクリーンショット 2022-06-10 9.59.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/9494e5ce-627f-4803-a9c8-8fca39da77ff.png)

##### ↓確認ダイアログ
![スクリーンショット 2022-06-10 10.02.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/e0e8a58d-ab64-f0dd-f865-d50d88bc579f.png)


娘「↑こんなイメージなの」

ワイ「なるほどな」

1. ユーザーが「登録」ボタンを押す。
1. `isOpen`変数が`true`になることで、確認ダイアログが表示される

ワイ「↑こんな感じやな」
ワイ「ほな、`isOpen`っていう変数が必要やな」
ワイ「ダイアログの**開閉状態**を表す変数やな」

娘「そうだね」
娘「あとは」

- 確認ダイアログ内の
  - 「OK」ボタンを押すと`ok`関数が実行される
  - 「キャンセル」ボタンを押すと`cancel`関数が実行される

娘「↑こんな機能も必要だから」
娘「`ok`関数と`cancel`関数も用意しないとだね」

ワイ「せやな」
ワイ「ってことは」

```diff_tsx
  const {
    myConfirm,
    isOpen,
    ok,
    cancel,
  } = useConfirm()
```

ワイ「↑こんな感じで、`useConfirm`っていう**カスタムフック**を作って」

- `myConfirm`関数
    - 確認ダイアログを起動するための関数
- `isOpen`変数
    - ダイアログの開閉状態を表す真偽値
- `ok`関数
    - 「OK」ボタンが押された時に実行する関数
- `cancel`関数
    - 「キャンセル」ボタンが押された時に実行する関数

ワイ「↑これらを、まとめて作ってくれるようにしておけば」
ワイ「色んな画面で使い回せて便利そうやな」

娘「そうだね！」

# カスタムフックを書いてみる

ワイ「とりあえず、概要だけ書いてみるで」

```diff_tsx:/hooks/useConfirm.tsx
export const useConfirm = () => {
  // 確認ダイアログを起動するための関数
  const myConfirm = () => {}

  return {
    myConfirm, // 確認ダイアログを起動するための関数
    isOpen, // ダイアログの開閉状態
    ok: () => {}, // 「OK」ボタン用の関数
    cancel: () => {}, // 「キャンセル」ボタン用の関数
  }
}
```

ワイ「↑こんな感じやな」
ワイ「`useConfirm`を実行すれば、返り値として**必要な関数と変数**を受け取れるイメージや」

娘「そうだね」
娘「それで、`isOpen`はダイアログの開閉状態を表すから」
娘「状態管理が必要だね」
娘「だから・・・」

```diff_tsx:/hooks/useConfirm.tsx
// このカスタムフックで管理する状態を表す型
type State = {
  // ダイアログの開閉状態
  isOpen: boolean
}

// 状態の初期値
const initialState: State = {
  // ダイアログは閉じている
  isOpen: false,
}
```

娘「↑こんなステート（状態）を定義しておくね」

ワイ「なるほどな」
ワイ「ほんで、確認ダイアログを起動するための`myConfirm`関数を実行したら」
ワイ「`isOpen`が`true`になるようにせんとアカンから・・・」

```diff_tsx:/hooks/useConfirm.tsx
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()`されるようにする

娘「↑こういう非同期処理をしたいわけだから」

ワイ「せやな」
ワイ「ほな・・・」

```diff_tsx:/hooks/useConfirm.tsx
  // 確認ダイアログを起動するための関数
  const myConfirm = () => {
+   const promise: Promise<boolean> =
+     new Promise((resolve) => {
        // 新しい状態を定義。
        const newState: State = {
        isOpen: true, // ダイアログを開いた状態に。
        }

        // 状態を更新。
        setState(newState)
+     })
+   return promise
  }
```

ワイ「↑こんな感じやな」
ワイ「これで`myConfirm`関数は、ダイアログを開きつつ、`Promise`オブジェクトを生成して返す」
ワイ「そんな関数になったわけやな」

娘「そうだね・・・あっ」
娘「このときに、**`resolve`もステートに保存**しておけば良いんじゃない？」

ワイ「と言いますと？」

娘「えっとね」

```diff_tsx:/hooks/useConfirm.tsx
  // 確認ダイアログを起動するための関数
  const myConfirm = () => {
    const promise: Promise<boolean> =
      new Promise((resolve) => {
      // 新しい状態を定義。
      const newState: State = {
        isOpen: true, // ダイアログを開いた状態に。
+       resolve, // resolve関数をステートに保存。
      }

      // 状態を更新。
      setState(newState)
      })
    return promise
  }
```

娘「↑こうだね」

ワイ「へぇ、`resolve`関数をステートに保存するんや・・・」

娘「そう」
娘「だから、ちゃんとステートの型も」

```diff_tsx:/hooks/useConfirm.tsx
// このカスタムフックで管理する状態を表す型
type State = {
  // ダイアログの開閉状態
  isOpen: boolean
+ // resolve格納用
+ resolve: (bool: boolean) => void
}
```

娘「↑こう変えてあげて」
娘「ステートの初期値も」

```diff_tsx:/hooks/useConfirm.tsx
// 状態の初期値
const initialState: State = {
  // ダイアログは閉じている
  isOpen: false,
+ // 何もしない関数
+ resolve: () => { },
}
```

娘「↑こう変えておかないとね」

ワイ「ふむふむ」
ワイ「ちゃんと、型と初期値も変えてやらんとな」
ワイ「それで、`resolve`関数をステートに保存すると、何かいいことあるん？」

娘「それはね」

```diff_tsx:/hooks/useConfirm.tsx
export const useConfirm = () => {
  // 状態を管理
  const [state, setState] = useState<State>(initialState)

  /* --- 省略 --- */

  return {
    myConfirm, // 確認ダイアログを起動するための関数
    isOpen: state.isOpen, // ダイアログの開閉状態
+   resolve: state.resolve, // resolve関数
  }
}
```

娘「↑こんな感じで、`resolve`関数を返してあげられるようになるの」

ワイ「おお、なるほど」
ワイ「`useConfirm()`が`resolve`を返してあげるんやね」
ワイ「そうすることで、ページ側で`resolve`関数を受け取って」
ワイ「ボタンに割り当てることができるんやな」
ワイ「つまり」

```diff_tsx:登録ページのtsx
// ボタンをクリックすると Promise が resolve される
<button onClick={resolve}>OK</button>
```

ワイ「↑こんなイメージやな」

娘「そうそう」

ワイ「あれ？でも」
ワイ「ボタン用に返してあげたい関数は」
ワイ「`resolve`やなくて」

- 「OK」ボタン用の`ok`関数
- 「キャンセル」ボタン用の`cancel`関数

ワイ「↑この2つやなかったっけ？」

娘「そういえばそうだったね」
娘「じゃあ、`resolve`関数をラップした`ok`関数と`calcel`関数を作って」
娘「それを返してあげることにするね」

```diff_tsx:/hooks/useConfirm.tsx
  // 「OK」ボタン用の関数
  const ok = () => {
    // resolveしてあげる
    state.resolve(true)
    // 状態は初期化。
    setState(initialState)
  }

  // 「キャンセル」ボタン用の関数
  const cancel = () => {
    // resolveしてあげる
    state.resolve(false)
    // 状態は初期化。
    setState(initialState)
  }
```

娘「↑こうして」

```diff_tsx:/hooks/useConfirm.tsx
  return {
    myConfirm, // 確認ダイアログを起動するための関数
    isOpen: state.isOpen, // ダイアログの開閉状態
-   resolve: state.resolve, // resolve関数
+   ok, // 「OK」ボタン用の関数
+   cancel, // 「キャンセル」ボタン用の関数
  }
```

娘「↑こうだね」

ワイ「なるほどなぁ」
ワイ「こうやって、`resolve`を戻り値の一部として」
ワイ「`useConfirm`の外に返してあげれば」
ワイ「ボタンに割り当てることができるんやな」

娘「そんな感じだね」

ワイ「ほぇ〜、ところで」
ワイ「`ok`関数の中の`state.resolve(true)`って部分と」
ワイ「`cancel`関数の中の`state.resolve(false)`って部分で」
ワイ「**真偽値**を渡してるのは何なん？」

娘「それはね」


```diff_tsx
  // 登録ボタンを押すと実行される関数
  const onSubmit = async () => {
    // 確認ダイアログを表示し、
    // ユーザーが「OK」か「キャンセル」を押すまで待機する。
    const bool = await myConfirm()

    // ユーザーが「OK」か「キャンセル」を押すと、以下が実行される。
    if (bool) {
      window.alert("ここで登録処理を実行。")
    } else {
      window.alert("キャンセルなので何もしない。")
    }
  }
```

娘「↑ここの`bool`に入ってくるの」

ワイ「ああ、そうか」
ワイ「`resolve()`するときに渡す値が」
ワイ「`await myConfirm()`したときの返り値になるんやね」
ワイ「そんで、`OK`が押されたのか、`キャンセル`が押されたのか」
ワイ「それを真偽値で判断できるんやな」

娘「そうそう」

ワイ「よっしゃ、これで」

- ユーザーがボタンを押したら、`resolve()`される

ワイ「つまり」

- ユーザーがボタンを押すまで、処理を待機する

ワイ「↑これが実現できたな！」
ワイ「使い方は・・・」

```diff_tsx
  const {
    myConfirm,
    isOpen,
    ok,
    cancel,
  } = useConfirm()

  // 登録ボタンを押すと実行される関数
  const onSubmit = async () => {
    // 確認ダイアログを表示し、
    // ユーザーが「OK」か「キャンセル」を押すまで待機する。
    const bool = await myConfirm()

    // ユーザーが「OK」か「キャンセル」を押すと、以下が実行される。
    if (bool) {
      window.alert("ここで登録処理を実行。")
    } else {
      window.alert("キャンセルなので何もしない。")
    }
  }
```

```diff_tsx:登録ページのtsx
    <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くんから教えてもらったからな」
ワイ「長いから、**折りたたみ表示**にしておくで」

<details><summary>ページ側</summary>

```diff_tsx:登録ページのtsx
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
```
</details>

<details><summary>カスタムフック側</summary>

```diff_tsx:/hooks/useConfirm.tsx
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, // 確認ダイアログ描画関数
  }
}
```
</details>

ワイ「↑こうやな」

娘「けっこう使いやすそうだね」

ワイ「`登録してもよろしいですか？`の文言だけは、引数で渡すようにして」
ワイ「ページごとに変えられた方がいいかもな」

娘「そうだね」
娘「それは自分でやってみる」
娘「ありがとう、パパ！」

ワイ「へへ、今回はなかなか大変やったな」
ワイ「ところで娘ちゃん」

![スクリーンショット 2022-06-10 10.02.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295452/e0e8a58d-ab64-f0dd-f865-d50d88bc579f.png)

ワイ「↑こんな地味なデザインなら」
ワイ「わざわざオリジナルで作らなくても」
ワイ「`window.confirm()`でええんちゃうか・・・？」

娘「ほんとだ、独自で作る意味ないね！」
娘「じゃあ、`window.confirm()`を使うことにする！」

ワイ「ファーーーーー（失神）」

**〜今度こそおしまい〜**


# ちなみに
- カスタムフックが返す内容は`useMemo()`や`useCallback()`してから返した方が、再レンダリングを抑えられて良いと思います。
  （今回は省略しました）
- ダイアログコンポーネントは、どんな場所で呼び出しても`z-index`的に大丈夫なものを使用している想定です。
  （`Material-UI`の`Dialog`など）
- `window.confirm()`と違うデザインの確認ダイアログを表示したい場合や、ダイアログに見出しを追加したい場合などに良さそうですね。
- ほとんど同じやり方で`window.alert()`のようなカスタムフックも作れます。


# 新しい記事もよろしくやで
- [ご年配のお客様「公開鍵とは何ですじゃ？」](https://qiita.com/Yametaro/items/223d4b3c804e05037578)