Reactでモーダル外をクリックしても閉じるモーダルの作り方と注意点【関数コンポーネント編】

こんにちは!かたつむり(@Katatumuri_nyan)です!

Reactでモーダルウィンドウを作る時に、「閉じるボタンだけじゃなくて、モーダル外をクリックしても閉じるようにしたい」って思ったのですが、なかなか苦戦しましたw

他にも同じように困っている方がいるかも!と思い、今回はモーダルの作り方をまとめてみました(*´ω`)

今回は関数コンポーネントで作成した例です♪
↓クラスコンポーネントで作成したい方はこちらの記事から↓
Reactでモーダル外をクリックしても閉じるモーダルの作り方と注意点【クラスコンポーネント編】

モーダルのデモ

このデモで表示されているコードと、実際のコードは少し違います😢
実際のコードを見てみたい方はこちらから↓

GitHubでコードを見る

モーダルを作る流れ

色んなやり方があると思うのですが、私はこんな流れでモーダルを作ってみました↓

1. モーダルを表示したいページのコンポーネント(親)を作成
2. モーダルコンポーネント(子)を作成
3. ページとモーダルをつなげる
4. CSSの設定
5. ボタンを押すとモーダルが開く設定
6. ボタンを押すとモーダルがとじる設定
7. モーダル外を押しても閉じる設定

ページ用のコンポーネントとモーダルのコンポーネントを分けて作りました。
では、実際にモーダルを作っていきましょう!

ぞれでは、順番に作っていきたいと思います!

コードの完成形

まずはコードの完成形を見ておきましょう~!

1. モーダルを表示したいページのコンポーネント(親)を作成

まずは、ページのコンポーネント(親)を作っていきます!
既にページがある場合は、参考程度に見てみてくださいね♪

ここでは、MyComponents.jsというファイル名にします。

このページのコンポーネント(親)をサイト上に表示させる方法は省略します~!

2. モーダルコンポーネント(子)を用意

次に、モーダルコンポーネントを作っていきます。
今回は分かりやすく説明するため、親コンポーネントと同じファイルで、親コンポーネントの上に追加で書いています。

3. ページとモーダルをつなげる

次に、ページコンポーネント(親)にモーダルコンポーネント(子)を挿入していきます。

これで、モーダルが表示されたんじゃないかと思います~!

4. CSSの設定

次は、CSSを設定していきます。
今のままだと、モーダルが変な所に挿入されちゃうので、浮かせますw
CSSの説明は飛ばします~!
私は以下のように設定しました!

ここでは、modal.cssというファイル名にして、MyComponents.jsと同じディレクトリ(ファイル)に入れています。

そして、コンポーネントを書いているMyComponents.jsに以下を追加します。
これでCSSが適応されるはずです!

5. ボタンを押すとモーダルが開く設定

モーダルが浮きましたね~!
それでは、次は、ボタンを押すとモーダルが開くようにします!

コードの説明をしていきます。

ステートの設定

今回は、モーダルの開閉にステートを使うので、useStateをインポートします。

今回は、isModalOpenというステートを設定しました。
isModalOpentrueの時にモーダルが開いて、falseの時にモーダルが閉じるように作っていきます~!
↑の記述で初期値をfalseにしたので、最初は閉じてることになりますね。

モーダルを開く関数の作成。

openModal()という関数を作成しました。
openModal()が発動すると、setIsModalOpen()が、isModalOpenステートをtrueにしてくれます。
isModalOpentrueの時にモーダルが開く)

buttonタグにonClickというイベントハンドラ(クリックされたとき○○をするっていうやつ)を設定します。
クリックされたらopenModal()が発動するように記述します。

モーダルコンポーネント(子)がステートによって開閉する設定

さっきまでの状態だと、モーダルは表示されたままですよね。
モーダルをisModalOpenの状態によって開閉するように書いていきます。
(条件)?(trueの時):(falseの時)というif文の省略形を使っています。

今回は、this.state.isModalOpentrueの時<Modal />falseの時""ということですね。

これでモーダルがボタンを押すと開くようになりましたね(*´ω`)

6. 閉じる ボタンを押すとモーダルが閉じる設定

閉じるボタンを押すと、モーダルが閉じるコードを書いていきます。

まずは親コンポーネントへの記述を説明していきます!

モーダルを閉じる関数の作成

openModal()の時と同様に、setIsModalOpen()関数を使っていきます。
ここでは、閉じる関数なのでisModalOpenfalseになるようにします。

モーダルコンポーネントにcloseModal()関数を渡す

onClick={()=>{closeModal()}}が増えました!
紛らわしいですが、onClickイベントハンドラではないです。
ただonClickって名前をつけてモーダルコンポーネントに関数を渡しています。

モーダルコンポーネントにある閉じるボタンを押すと、親コンポーネントのcloseModal()関数が実行されるようにしたいんです!
でも、モーダルコンポーネントから直接親コンポーネントの関数を実行できないので、一度onClickという名前でモーダルコンポーネントに関数を渡しているって感じです。

モーダルコンポーネントで関数を受け取る

次にモーダルコンポーネントでモーダルを閉じるボタンの設定をしていきます!

まずは、親コンポーネントのpropsを受け取りたいので、モーダルコンポーネントにprops引数を設定しておきます。
親コンポーネントから値を引き継ぐことができるおまじないですw

こっちのbuttunタグについているのはonClickイベントハンドラです!
クリックされた時に、props.onClickを発動するという記述です。

props.onClickで、親コンポーネント側で設定したonClickの中身(closeModal()関数)を取り出せます。
これで、モーダルコンポーネント側から、親コンポーネントのisModalOpenステートを更新できます。

7. モーダル外を押しても閉じる設定1/2

さて、ここからが本題です!(長かった)
モーダルの外を押してもモーダルが閉じるようにしないといけません。

順番ばらばらで見にくいですが、説明していきますね~!

画面をクリックしたときにモーダルが閉じるようにする

document.addEventListener('click',closeModal)で、画面のクリックイベントを監視します。
画面がクリックされたら、closeModalを発動してくれます。
エラーが出るかもですが、後のためにここではcloseModalと書いておいてください!

でも、このままだと、openModal()関数が発動したらすぐにモーダルが閉じてしまいます(´;ω;`)
openModal()はモーダルを開くボタンをクリックすることによって発動するので、
開くボタンのクリックでaddEventListenerが発動しまって、モーダルが閉じるというわけですね(;´・ω・)

そこで、event引数を追加して、イベント情報を受け取れるようにします。
そして、event.stopPropagation()と書きます。
これで、openModal()を発動させる時のクリックは、addEventListenerの適用外になります。

openModal()関数にクリックされたときのイベント情報を渡してあげるため、eventを追加します。

モーダルが閉じた時にaddEventListenerの監視を止める

今のままだと、モーダルが閉じてもdocument.addEventListener('click',this.closeModal)の監視が続いてしまいます。
そこで、removeEventListenerを使ってaddEventListenerの監視を解除します。

7. モーダル外を押しても閉じる設定2/2

2/2て!長いわ!って感じですが、ここからが大事なのです!

アンマウント時のメモリリーク対策をする

今のままだと、モーダルを開いたまま別のページに遷移した時などに、エラーが起きてしまいます。
どうしてかというと、モーダルを開いた時に発動したdocument.addEventListener('click',closeModal)の監視が、別のページでも続くからなんです。
そこで、メモリリークの対策をしていきます!

まずは、useEffectuseCallbackを使うので、インポートしておきます。

useEffectは、コンポーネントが表示・非表示になったタイミングで発動してくれます。(厳密には違います。)
returnとして書いたものは、コンポーネントが非表示になった直後に発動します。

そこで今回は、returnの処理として、removeEventListenerを使ってaddEventListenerを解除しておくわけですね(*´ω`)
(説明難しい…(;´・ω・))

useEffectの第二引数([]の中)には依存関係を書きます。
今回はcloseModalに依存しているので、closeModalを書いておきます。
(ここ難しいので詳しく知りたい方は調べてみてください!)

useEffectも大事ですが、useCallbackも大事です。
useEffectの中で修正前↑のcloseModal()関数を呼び出すと、大変なエラーが起きてしまいますw

そこで、以下のように修正します。

useCallbackで包んであげると、あら不思議、いい感じに動いてくれます。
useCallbackは、メモ化されたコールバックを返すとか色々あるのですが、ここではuseEffectとセットで使うといい感じくらいに思っておいてください。

モーダルの中をクリックしたときに閉じないようにする。

ここまでで、画面をクリックしたときにモーダルが閉じるようになったと思います(*´ω`)
だけど、閉じるボタン以外のモーダルの要素をクリックしてもモーダルが閉じてしまいますよね。

そこで、モーダルコンポーネントで以下の処理を行います。

モーダル要素の一番外側にonClickイベントハンドラを渡して、event.stopPropagation()と書いておきます。
こうすることで、モーダルを開くボタンの時と同様にモーダルコンポーネントを、addEventListenerの適用外にできます。

これでモーダルの完成です~!
お疲れ様でした♪

モーダル作成時の注意点

アンマウント時のメモリリーク対策ができていないと、以下の様なエラーが出ます(´;ω;`)

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at Modal_FunctionComponent

↑のように記述することで、対策できるので、気を付けてみてください♪

参考

React Hooksを使ったモーダル実装

安全に React Hooks を使用する

バブリングによるイベントの伝播

Event.stopPropagation()

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です