Laravel + React 独自の confirm() をシンプルに使えるようにする

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、Laravel & Reactを使った開発も少し慣れてきた今日この頃です。

そして、これは皆さん同じだと思うのですが、何でも慣れてきたら「できるだけ楽して同じ効果を出したい」と思うんじゃないでしょうか。

つまり、コンポーネントや共通コードをつくって「使いまわし」ができるようにする「ショートカット」ですね。

ただ、この間このコンポーネントさえも「何回も使ってめんどう…😫」という場面に遭遇しました。

それが…

独自の confirm() モーダル

です。

↓↓↓ こういうやつですね。

というのも、基本的なショートカットとしてはこのようなモーダルを「コンポーネント化」するわけですが、同じページで複数必要な場合、何度も似たコード書かないといけなくなりメンドウになってしまいます(useStateで{表示/非表示}部分をすべてのモーダルにつくるのは 😫 となります)

そこでなんとかJavaScriptに標準搭載されているconfirm()のように使えないだろうか🤔と考えた結果が今回の記事になります。

↓↓↓ こんな感じです。

if(confirm('よろしいですか?')) {
    
    // ここで OK された時の処理
    
}

ぜひ何かの参考になりましたら嬉しいです。😄✨

「クラフトビールの価値を
普通のビールと同じに
考えちゃダメ❗ゼッタイ🍺✨」

やりたいこと

冒頭でも書きましたが、JavaScriptに標準搭載されているconfirm()のような使い方」ができるようにしていきます。

ただ、結果としてはif()での分岐はブラウザ側をいじらないといけないと思いましたので、最終的には以下のように実装しました。

modalRef.current.show(message)
    .then(() => {

        console.log('OK のとき')
        
    })
    .catch(() => {

        console.log('キャンセルのとき')
        
    });

では、その研究結果をご報告します❗

独自 confirm() モーダル・コンポーネントをつくる

いきなりですが、confirm()の代わりとなるコンポーネントConfirmationModalをつくっていきます。

resources/js/Components/ConfirmationModal.jsx

import {forwardRef, useImperativeHandle, useRef, useState} from "react";

export default forwardRef((props, ref) => {

    // Data
    const [showModal, setShowModal] = useState(false);
    const [message, setMessage] = useState('');

    // from Parent
    const callbacksRef = useRef({});
    useImperativeHandle(ref, () => ({

        show(message) {

            setShowModal(true);
            setMessage(message);

            return new Promise((resolve, reject) => {

                callbacksRef.current = {resolve, reject}; // ここで Promise の結果実行関数を保存しておく

            });

        },

    }));

    // Click
    const handleOkClick = () => {

        setShowModal(false);
        callbacksRef.current.resolve();

    };
    const handleCancelClick = () => {

        setShowModal(false);
        callbacksRef.current.reject();

    };

    return (
        <>
            {showModal && (
                <div className="relative z-10">
                    <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
                    <div className="fixed inset-0 z-10">
                        <div className="flex min-h-full items-end justify-center p-4 text-center items-center">
                            <div className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all w-96">
                                <div className="bg-white p-4">
                                    <div className="flex items-start">
                                        <div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100">
                                            <svg className="h-6 w-6 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true">
                                                <path strokeLinecap="round" strokeLinejoin="round" d="M12 10.5v3.75m-9.303 3.376C1.83 19.126 2.914 21 4.645 21h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 4.88c-.866-1.501-3.032-1.501-3.898 0L2.697 17.626zM12 17.25h.007v.008H12v-.008z" />
                                            </svg>
                                        </div>
                                        <div className="mt-3 mt-0 ml-4 w-full">
                                            {message}
                                        </div>
                                    </div>
                                </div>
                                <div className="bg-gray-200 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
                                    <button
                                        type="button"
                                        className="rounded-md border border-gray-300 bg-blue-600 px-4 py-2 text-base font-medium text-white"
                                        onClick={handleOkClick}>
                                        OK
                                    </button>
                                    <button
                                        type="button"
                                        className="rounded-md border border-gray-300 bg-gray-50 px-4 py-2 text-base font-medium text-gray-700 mr-2"
                                        onClick={handleCancelClick}>
                                        キャンセル
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            )}
        </>
    );

});

今回このコンポーネントを作るときに初めてReactforwardRef、 useImperativeHandleを使うことになりました。

これらを使うと、なんと「コンポーネント内にあるメソッドを(外側から)実行できるようになる」んですね。

つまり、以下の例でいうとYourComponentの中に定義されているhello()をメインページから実行することができるようになります。

【メインページ】

import ConfirmationModal from "@/Components/YourComponent";

// 省略

// ここで YourComponent の hello() が実行できる👍

return (
    <>
        <YourComponent></YourComponent>
    </>
);

【コンポーネント】

// 省略

useImperativeHandle(ref, () => ({

    hello() {

        // メインページから実行できる👍

    },

}));

// 省略

こうすることで、何度も{表示/非表示}切り替えをメインページでしなくてよくなります。

そして、if()分岐の代わりに使ったのがPromiseです。

Promiseは、めちゃくちゃ簡単に言うと「タイミングをずらして分岐できる」機能です。

今回のケースで言うと、「OK もしくは、キャンセルが決定されるのは使っているユーザーのタイミング」になるので、モーダルが表示されたときにresolverejectを保持しておいて、実際の選択時に最終決定できるようにしています。

これで、以下のような実装ができるようになりました。(axiosっぽくしたかったんですね😄)

modalRef.current.show(message)
    .then(() => {

        // OK のとき

    })
    .catch(() => {

        // キャンセルのとき

    });

独自コンポーネントを使う

では、先ほどつくったConfirmationModalを使ってみましょう。

今回は同じページで3つの確認モーダルが表示できるようにしますが、使っているコンポーネントはたった1つだけという点に注目してください。

resources/js/Pages/ConfirmationModal/Index.jsx

import ConfirmationModal from "@/Components/ConfirmationModal";
import {useRef} from "react";

export default function Index() {

    // Modal
    const modalRef = useRef();
    const handleOpenModalOne = () => {

        const message = 'よろしいですか?(1番目のボタン)';
        modalRef.current.show(message)
            .then(() => console.log('OK がクリックされました(1番目のボタン)'))
            .catch(() => console.log('キャンセルがクリックされました(1番目のボタン)'));

    };
    const handleOpenModalTwo = () => {

        const message = 'よろしいですか?(2番目のボタン)';
        modalRef.current.show(message)
            .then(() => console.log('OK がクリックされました(2番目のボタン)'))
            .catch(() => console.log('キャンセルがクリックされました(2番目のボタン)'));

    };
    const handleOpenModalThree = () => {

        const message = 'よろしいですか?(3番目のボタン)';
        modalRef.current.show(message)
            .then(() => console.log('OK がクリックされました(3番目のボタン)'))
            .catch(() => console.log('キャンセルがクリックされました(3番目のボタン)'));

    };

    return (
        <div className="p-5">
            <button
                type="button"
                className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
                onClick={handleOpenModalOne}>
                モーダルを表示する(1番目のボタン)
            </button>
            <br />
            <button
                type="button"
                className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
                onClick={handleOpenModalTwo}>
                モーダルを表示する(2番目のボタン)
            </button>
            <br />
            <button
                type="button"
                className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
                onClick={handleOpenModalThree}>
                モーダルを表示する(3番目のボタン)
            </button>

            {/* モーダル */}
            <ConfirmationModal ref={modalRef}></ConfirmationModal>
        </div>
    );

}

ConfirmationModalの使い方はシンプルです。

まず、useRef()から作ったmodalRefを用意し、以下のようにrefへセットするだけで完了です。

const modalRef = useRef();

// 省略

<ConfirmationModal ref={modalRef}></ConfirmationModal>

あとは、確認モーダルを表示したいときに、show()を呼び出し、axiosのようにドットでつないでthen or catchで分岐をします。

ルートをつくる

では、先ほどのページがLaravelで実行できるようにルートを追加しておいてください。

routes/web.php

// 省略

Route::get('confirmation_modal', fn() => Inertia::render('ConfirmationModal/Index'));

テストしてみる

では実際にテストしてみましょう❗

まずはViteを起動して、「http://******/confirmation_modal」へアクセスしてください。

すると・・・・・・

はい❗
ボタンが表示されました。

では、まずは「1番目のボタン」をクリックしてみましょう。

どうなるでしょうか・・・・・・

はい❗
モーダルが表示されました。

では、ブラウザのコンソールを開いて、まずは「OK」ボタンをクリックしてみましょう。

うまくいくでしょうか・・・・・・

はい❗
コンソールにうまく表示がでました。

では、次に「2番目のボタンをクリックし、キャンセルを選択」してみましょう。

すると・・・・・・

はい❗
こちらもうまく表示が出ました。

すべて成功です😄✨

デモページを用意しました

せっかくなのでデモページを用意しました。
ぜひ実際に確かめてみてください。

📝 デモページ

企業様へのご提案

今回のように特によく利用する機能は、通常のコンポーネント化ではない方法を利用することでより省コード化することができ作業効率がアップします。

そういった開発をご希望でしたら、ぜひお問い合わせからご連絡ください。

お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はconfirm()を代替する独自モーダルをつくってみました。

今回はconfirm()でしたが、alert()でも同じですので、今回のコードを改造してご自身で作ってみてくださいね。

ではでは〜❗

「グルメな友人御用達
のお寿司屋さんウマすぎでした❗
(しかもお値ごろ 😄✨)」

このエントリーをはてなブックマークに追加       follow us in feedly