JavaScript
reactjs

[検証中]Reactで初回render時にcomponentDidUpdateが呼び出される時の対処法

※注意※

この記事の対処法は現在検証中です。
また、componentDidUpdate()が呼び出される原因についても別の要因が考えられたため、
問題を一つずつ分解していき、わかり次第記事に追記していきたいと思います。

問題

Reactのライフサイクルで、初回読み込み時に呼び出されるメソッドはcomponentWillMount(),render(),componentDidMount()だが、SPAを作っていてcomponentDidUpdate()も同時に呼び出されてしまうという現象にハマった。

備忘録として、原因や解決法をメモしておきます。

原因

render()内で非同期的に子コンポーネントを描画する処理を書いているとそれがトリガーとなり、初回ページの読み込み時にもcomponentDidUpdate()が呼び出されてしまう。

SPAの構造

  • App (親コンポーネント)
  • Message (子コンポーネント)

Appは配列でmessagesというstateを持ち、初回にFirebaseなどの外部DBに接続し、格納されているテキスト形式のメッセージを取得する。
Appのrender()内で取得したmessagesをmapメソッドでひとつずつ個々のMessagesコンポーネントとしてreturn()する。

具体的にはこういう処理が原因

class Appのrender()内でmapメソッドを使うと、配列の要素の数だけ複数回render()が走ってしまう。このため、クライアント側で新たに操作を行わなくてもcomponentDidUpdate()が走ってしまうようです。
↑ウソ。外部DB接続をせず、コード内にmessagesの内容をベタ書きしてsetStateしてみたところ、componentDidUpdate()は走らなかった。したがって、mapメソッド自体に副作用はない。
では原因は何かというとまだ不明・・・。

App.js
export default class App extends Component {
  constructor(props){
    super(props)
    this.state = {
      messages : []
    }
  }

  //-- DBからのメッセージ読み込み (FirebaseのRealtime Database使用)
  componentWillMount(){
    messagesRef.on('value', (snapshot) => {
      const messages = []
      const val = snapshot.val()
      Object.keys(val).forEach(function (key) {
        console.log(key)
        messages.push(val[key])
      });

      this.setState({
        messages : messages // stateに取得したmessagesを格納
      });
    });
  }

  componentDidMount() {
    console.log('Mounted')
  }

  componentDidUpdate() {
    console.log('Updated') // <- 初回読み込み時にトリガーされる
  }

    render() {
    return (
      <div className="App">
                //---------子コンポーネントの描画---------
        <div className="Messages">
          {this.state.messages.map((m, i) => {
            return (
              <div className="message" key={i}>
                <Message message={m}/> // <- メッセージの内容をpropsで渡す
              </div> )
            })
          }
        </div>
      </div>
    )
  }
}
Messages.js
const Message = (props) => {
  return (
    <div className="message">
      {props.message.text}
    </div>
  )
}

export default Message

対処法

親コンポーネントのrender()メソッド内では極力mapメソッドの使用を避ける。そのためには、<RenderMessages />とかの名前で新たに<Messages />をラップするステートレスなコンポーネントを作って、propsでmessagesを渡してあげて、その中でmapメソッドを使って展開させてやるとcomponentDidUpdate()の無用な呼び出しを避けられそうです。
理想としてはこんな感じ。

-> mapメソッド自体は副作用をもたらさないので、親コンポーネントのrender()で使用をしても問題はない。
とはいえ、あまり親コンポーネント内で即時関数を使いたくないなぁとも思うので以下のようにラップするコンポーネントを別途用意するのはコンポーネントの設計的にもベターな気がします。ただし、propsのバケツリレーが始まってしまうので頻繁に使うのもためらわれるというジレンマ。

RenderMessages.js
const RenderMessages = (messages) => {
  return(
    {messages.map((m, i) => {
      return (
        <div className="message" key={i}>
          <Message message={m}/> // <- メッセージの内容をpropsで渡す
        </div> )
      })
    }
  )
}
export default RenderMessages

これでちゃんと動くかどうかはまだ検証していないので結果がわかったらまた追記します。

参考

React & Firebaseで簡単なChatアプリを作ってみた
https://qiita.com/kazushikawamura/items/58ea222b3cc289882d79