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

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

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

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

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

モーダルのデモ

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

GitHubでコードを見る
デモサイトもあります!

モーダルを作る流れ

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

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

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

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

コードの完成形

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

 // MyComponents.js
import React from 'react';
import './modals.css';

// 子コンポーネント(モーダル)
class Modal extends React.Component {

  render(){
    return(
        <div id="modal" className="modal" onClick={(event)=>{event.stopPropagation()}}>
          <div>
            <p>モーダル</p>
            <button onClick={this.props.onClick}>閉じるボタン</button>
          </div>
        </div>
    )
}}

// 親コンポーネント
class Modal_ClassComponent extends React.Component {
  constructor(props) {
   super(props);
    this.state = {
      isModalOpen: false
    };
    this.closeModal = this.closeModal.bind(this);
  };

  componentWillUnmount(){
    document.removeEventListener('click',this.closeModal)
  }

  openModal(event){
    this.setState({isModalOpen:true})
    document.addEventListener('click',this.closeModal)
    event.stopPropagation()
  }

  closeModal(){
    this.setState({isModalOpen:false})
    document.removeEventListener('click',this.closeModal)
  }

  render(){
      return (
        <div id="modalpage" className="modalpage">
          <h2>クラスコンポーネント</h2>
            <button onClick={(event)=>{this.openModal(event)}}>モーダルを開く</button>

            {this.state.isModalOpen? <Modal onClick={()=>{this.closeModal()}}/> :""}

          </div>
      );
    }
}

export default Modal_ClassComponent;

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

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

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

// MyComponents.js
// Reactのインポート
import React from 'react';

// 親コンポーネント
class Modal_ClassComponent extends React.Component {

  render(){
      return (
        <div id="modalpage" className="modalpage">
          <h2>クラスコンポーネント</h2>
          <button>モーダルを開く</button>
          // ここにモーダルコンポーネント(子)を挿入したい。
        </div>
      );
    }
}

// 他のファイルで読み込めるようにexportしている
export default Modal_ClassComponent;

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

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

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

// MyComponents.js
// 子コンポーネント(モーダル)
class Modal extends React.Component {

  render(){
    return(
        <div id="modal" className="modal">
          <div>
            <p>モーダル</p>
            <button>閉じるボタン</button>
          </div>
        </div>
    )
}}

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

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

// MyComponents.js
// 親コンポーネント
class Modal_ClassComponent extends React.Component {

  render(){
      return (
        <div id="modalpage" className="modalpage">
          <h2>クラスコンポーネント</h2>
          <button>モーダルを開く</button>
          // ↓に挿入
          <Modal />
          // ↑に挿入
          </div>
      );
    }
}

export default Modal_ClassComponent;

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

4. CSSの設定

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

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

/* modal.css */
#modalpage{
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  color: pink;
  background-color: #FFEEF7;
  position: relative;
  margin: 0;
}

#modalpage .modal{
  /* ↓浮かせる設定 */
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
  -webkit-transform: translate(-50%, -50%);
  -ms-transform: translate(-50%, -50%);
  /* ↑浮かせる設定 */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 300px;
  height:200px;
  background-color: #FFF9F9;
  box-shadow: 0 3px 6px 3px rgba(107,107,107,.10);
  color: #FF9ECA;
  border-radius: 10px;
  text-align: center;
}

#modalpage button{
  width: 100px;
  height: 40px;
  border-radius: 10px;
  color: #FFF;
  background-color: #FBB1BF;
  border: none;
  box-shadow: 0 3px 6px 3px rgba(107,107,107,.10);
  margin: 20px;
}

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

// MyComponents.js
import React from 'react';
// ↓に挿入
import './modals.css';
// ↑に挿入

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

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

// MyComponents.js
// 親コンポーネント
class Modal_ClassComponent extends React.Component {
  // -----1------
  // ↓を追加
  constructor(props) {
   super(props);
    this.state = {
      isModalOpen: false
    };
  };
  // ↑を追加

  // -----2------
  // ↓を追加
  openModal(){
    this.setState({isModalOpen:true})
  }
  // ↑を追加

  render(){
      return (
        <div id="modalpage" className="modalpage">
          <h2>クラスコンポーネント</h2>
          // -----3------
          // ↓を修正
          <button onClick={()=>{this.openModal()}}>モーダルを開く</button>
          // ↑を修正

          // -----4------
          // ↓を修正
          {this.state.isModalOpen? <Modal /> :""}
          // ↑を修正
          </div>
      );
    }
}

export default Modal_ClassComponent;

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

ステートの設定

// -----1------
// ↓を追加
constructor(props) {
 super(props);
  this.state = {
    isModalOpen: false
  };
};
// ↑を追加

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

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

// -----2------
// ↓を追加
openModal(){
  this.setState({isModalOpen:true})
}
// ↑を追加

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

// -----3------
// ↓を修正
<button onClick={()=>{this.openModal()}}>モーダルを開く</button>
// ↑を修正

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

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

// -----4------
// ↓を追加
{this.state.isModalOpen? <Modal /> :""}
// ↑を追加

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

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

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

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

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

// MyComponents.js
// 親コンポーネント
class Modal_ClassComponent extends React.Component {
  constructor(props) {
   super(props);
    this.state = {
      isModalOpen: false
    };
  };

  openModal(){
    this.setState({isModalOpen:true})
  }

  // -----1------
  // ↓を追加
  closeModal(){
    this.setState({isModalOpen:false})
  }
  // ↑を追加

  render(){
      return (
        <div className="modalpage">
          <h2>クラスコンポーネント</h2>
            <button onClick={()=>{this.openModal()}}>モーダルを開く</button>
            // -----2------
            // ↓を修正
            {this.state.isModalOpen? <Modal onClick={()=>{this.closeModal()}}/> :""}
            // ↑を修正
          </div>
      );
    }
}

export default Modal_ClassComponent;

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

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

// -----1------
// ↓を追加
closeModal(){
  this.setState({isModalOpen:false})
}
// ↑を追加

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

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

// -----2------
// ↓を修正
{this.state.isModalOpen? <Modal onClick={()=>{this.closeModal()}}/> :""}
// ↑を修正

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

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

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

// MyComponents.js
// 子コンポーネント(モーダル)
class Modal extends React.Component {

  render(){
    return(
        <div id="modal" className="modal">
          <div>
            <p>モーダル</p>
            // -----3------
            // ↓を修正
            <button onClick={this.props.onClick}>閉じるボタン</button>
            // ↑を修正
          </div>
        </div>
    )
}}

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

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

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

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

// MyComponents.js
// 親コンポーネント
class Modal_ClassComponent extends React.Component {
  constructor(props) {
   super(props);
    this.state = {
      isModalOpen: false
    };
    // -----1------
    // ↓を追加
    this.closeModal = this.closeModal.bind(this);
    // ↑を追加
  };
  // -----5------
  // ↓を追加
  componentWillUnmount(){
    document.removeEventListener('click',this.closeModal)
  }
  // ↑を追加

  // -----2------
  // ↓を修正
  openModal(event){
    this.setState({isModalOpen:true})
    document.addEventListener('click',this.closeModal)
    event.stopPropagation()
  }
  // ↑を修正

  closeModal(){
    this.setState({isModalOpen:false})
    // -----4------
    // ↓を追加
    document.removeEventListener('click',this.closeModal)
    // ↑を追加
  }

  render(){
      return (
        <div className="modalpage">
          <h2>クラスコンポーネント</h2>
            // -----3------
            // ↓を修正
            <button onClick={(event)=>{this.openModal(event)}}>モーダルを開く</button>
            // ↑を修正
            {this.state.isModalOpen? <Modal onClick={()=>{this.closeModal()}}/> :""}

          </div>
      );
    }
}

export default Modal_ClassComponent;
// MyComponents.js
// 子コンポーネント(モーダル)
class Modal extends React.Component {

  render(){
    return(
        // -----6------
        // ↓を修正
        <div id="modal" className="modal" onClick={(event)=>{event.stopPropagation()}}>
        // ↑を修正
          <div>
            <p>モーダル</p>
            <button onClick={this.props.onClick}>閉じるボタン</button>
          </div>
        </div>
    )
}}

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

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

constructor(props) {
   super(props);
    this.state = {
      isModalOpen: false
    };
    // -----1------
    // ↓を追加
    this.closeModal = this.closeModal.bind(this);
    // ↑を追加
  };

まずは、closeModal()をバインドさせておきます。
こうすることで、この後の説明で登場するthis.closeModalがすべて同じものになると思っておいてください。

// -----2------
// ↓を修正
openModal(event){
  this.setState({isModalOpen:true})
  document.addEventListener('click',this.closeModal)
  event.stopPropagation()
}
// ↑を修正

document.addEventListener('click',this.closeModal)で、画面のクリックイベントを監視します。
openModal()関数が発動すると、画面のクリックを監視しはじめるってわけですね!

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

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

render(){
    return (
      <div className="modalpage">
        <h2>クラスコンポーネント</h2>
          // -----3------
          // ↓を修正
          <button onClick={(event)=>{this.openModal(event)}}>モーダルを開く</button>
          // ↑を修正
          {this.state.isModalOpen? <Modal onClick={()=>{this.closeModal()}}/> :""}

        </div>
    );
  }

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

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

closeModal(){
  this.setState({isModalOpen:false})
  // -----4------
  // ↓を追加
  document.removeEventListener('click',this.closeModal)
  // ↑を追加
}

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

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

// -----5------
// ↓を追加
componentWillUnmount(){
  document.removeEventListener('click',this.closeModal)
}
// ↑を追加

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

そこで、componentWillUnmount()というライフサイクルメソッドとやらを使います。

componentWillUnmount()の中に書いたものは、そのコンポーネントがアンマウント(非表示)された直後に発動します。

componentWillUnmount()の中にdocument.removeEventListener('click',this.closeModal)を書いておくと、別のページに遷移した時に監視を解除できます。

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

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

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

// MyComponents.js
// 子コンポーネント(モーダル)
class Modal extends React.Component {

  render(){
    return(
        // -----6------
        // ↓を修正
        <div id="modal" className="modal" onClick={(event)=>{event.stopPropagation()}}>
        // ↑を修正
          <div>
            <p>モーダル</p>
            <button onClick={this.props.onClick}>閉じるボタン</button>
          </div>
        </div>
    )
}}

モーダル要素の一番外側に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 the componentWillUnmount method.
at Modal_ClassComponent

componentWillUnmount(){
  document.removeEventListener('click',this.closeModal)
}

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

参考

【JavaScript】クリックイベントで取得したオブジェクトの使い方 まとめ

React.Componentで外部要素のevent bind,unbindを正しく行う

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

安全に React Hooks を使用する

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

Event.stopPropagation()

コメントを残す

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