196
125

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-06-12

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

今回の使用技術

  • 言語
    • TypeScript
  • フレームワーク
    • Next.js( React.js 製 )

とある休日

娘「ねぇ、パパ?」

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

娘「ユーザーさんのタイミングでPromiseresolve()することはできないの?」

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

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

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

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

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

よめ太郎「せや」
よめ太郎「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秒後に処理したい内容」をthen()メソッドに渡す

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

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

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

娘「あと、ほかにも」

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

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

ワイ「せやな」

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

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

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

ワイ「うーん、つまり」

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

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

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

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

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

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

ワイ「ほうほう」

娘「でもね」

スクリーンショット 2022-06-10 9.22.02.png

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

ワイ「ほうほう」

娘「それで」

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

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

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

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

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

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

ワイ「むむぅ・・・」

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

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

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

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

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

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

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

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

登録ページの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

↓確認ダイアログ

スクリーンショット 2022-06-10 10.02.34.png

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

ワイ「なるほどな」

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

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

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

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

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

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

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

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

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

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

娘「そうだね!」

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

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

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

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

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

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

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

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

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

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

/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: () => {}, // 「キャンセル」ボタン用の関数
  }
}

ワイ「↑こうやな」
ワイ「これで、ダイアログの開閉状態を表すisOpentrueになるから」
ワイ「確認ダイアログが開くはずや」

娘「そうだね」

ワイ「そういえば、Promiseも書くって話だったけど、どこに書けばいいんや?」

娘「myConfirm関数の中じゃないかな?」
娘「myConfirmを実行したときに」

  • 確認ダイアログを開く
  • それと同時に
    • ユーザーがボタンを押したらresolve()されるようにする

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

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

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

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

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

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

ワイ「と言いますと?」

娘「えっとね」

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

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

娘「↑こうだね」

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

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

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

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

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

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

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

娘「それはね」

/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関数を受け取って」
ワイ「ボタンに割り当てることができるんやな」
ワイ「つまり」

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

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

娘「そうそう」

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

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

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

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

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

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

娘「↑こうして」

/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)って部分で」
ワイ「真偽値を渡してるのは何なん?」

娘「それはね」

  // 登録ボタンを押すと実行される関数
  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("キャンセルなので何もしない。")
    }
  }
登録ページの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くんから教えてもらったからな」
ワイ「長いから、折りたたみ表示にしておくで」

ページ側
登録ページの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
カスタムフック側
/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, // 確認ダイアログ描画関数
  }
}

ワイ「↑こうやな」

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

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

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

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

スクリーンショット 2022-06-10 10.02.34.png

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

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

ワイ「ファーーーーー(失神)」

〜今度こそおしまい〜

ちなみに

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

新しい記事もよろしくやで

196
125
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
196
125

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?