dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

React 16.3.0で追加されたStrictModeコンポーネントについて

2018-04-01のJS: TypeScript 2.8、React 16.3.0、TensorFlow.js - JSer.info を読んで。

React 16.3.0 から StrictMode コンポーネントというものが追加されたらしい。
公式ドキュメントを読んでみます。


StrictMode とは

StrictMode はアプリの潜在的な問題を検出するために追加されたコンポーネントコンポーネントだが Fragment などと同じく UI として画面に表示されるものはない。
<StrictMode>...</StrictMode> で囲まれた子孫コンポーネントに対し、いくつかのチェックを行う。

また development モードでのみ動作し、 production build 時には影響を与えない。


ScrictMode がチェックしてくれること

今のところ以下。今後のリリースで機能は追加予定とのこと(Additional functionality will be added with future releases of React.)

  1. 安全でないライフサイクルメソッドの使用(Identifying components with unsafe lifecycles)
  2. レガシーな string ref の使用(Warning about legacy string ref API usage)
  3. 予期せぬ副作用の検出(Detecting unexpected side effects)
    • (検出するために、特定のライフサイクルメソッドを二度実行する)


1. 安全でないライフサイクルメソッドの使用

背景として、v16.3.0 以降は非同期レンダリングなどのサポートのために一部のライフサイクルメソッド

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

が今後削除予定となった。
参考:Update on Async Rendering - React Blog (こっちはまだ読んでない)

<StrictMode> の子孫コンポーネントでこれらのライフサイクルメソッドを使用しているものがあれば、ブラウザのコンソールで warning が出力される。

(サンプル)

import React, { Component, StrictMode } from 'react';

class UnsafeComponent extends Component {
  componentWillMount() {
    console.log('componentWillMount');
  }

  componentWillReceiveProps(props) {
    console.log('componentWillReceiveProps');
  }

  render() {
    return <div>Unsafe Component</div>;
  }
}

export default function App() {
  return (
    <StrictMode>
      <UnsafeComponent />
    </StrictMode>
  );
}

(結果) f:id:dackdive:20180404015423p:plain

Warning: Unsafe lifecycle methods were found within a strict-mode tree:
    in App
    in AppContainer

componentWillMount: Please update the following components to use componentDidMount instead: UnsafeComponent

componentWillReceiveProps: Please update the following components to use static getDerivedStateFromProps instead: UnsafeComponent

Learn more about this warning here:
https://fb.me/react-strict-mode-warnings

該当のコンポーネント名と使っているライフサイクルメソッドが表示されている。


2. レガシーな string ref の使用

ref を使ってコンポーネントを参照するための方法はこれまで2通りあって

  1. ref="input" のように文字列で指定する
  2. ref={(element) => this.input = element} のように callback 関数で指定する

このうち 1 の文字列で指定する方にはいくつか問題があったらしく、ドキュメントでも 2 の方法を推奨していた。
<StrictMode> の子孫コンポーネントで 1 の string ref を使っている箇所があると、こちらも同様にブラウザのコンソールでwarning が出る。

(サンプル)

import React, { Component, StrictMode } from 'react';

class LegacyRef extends Component {
  handleClick = () => {
    const name = this.refs.name.value;
    console.log('LegacyRef', name);
  };

  render() {
    return (
      <div>
        <input ref="name" />
        <button onClick={this.handleClick}>click me</button>
      </div>
    );
  }
}

class NewRef extends Component {
  constructor(props) {
    super(props);

    this.nameRef = React.createRef();
  }

  handleClick = () => {
    const name = this.nameRef.current.value;
    console.log('NewRef', name);
  };

  render() {
    return (
      <div>
        <input ref={this.nameRef} />
        <button onClick={this.handleClick}>click me</button>
      </div>
    );
  }
}

export default function App() {
  return (
    <StrictMode>
      <LegacyRef />
      <NewRef />
    </StrictMode>
  );
}

(結果) f:id:dackdive:20180404021000p:plain

Warning: A string ref, "name", has been found within a strict mode tree. String refs are a source of potential bugs and should be avoided. We recommend using createRef() instead.

    in div (created by LegacyRef)
    in LegacyRef (created by App)
    in App
    in AppContainer

Learn more about using refs safely here:
https://fb.me/react-strict-mode-string-ref


余談: createRef() を使った新しい ref の方法

上のサンプルで、 NewRef コンポーネントがやっているのは v16.3.0 から追加された新しい ref の実装方法で、 createRef() という関数を使う。
従来の string ref のような書き方で、かつ string ref のときの問題点は解決されている。らしい。

参考:React v16.3.0: New lifecycles and context API - React BlogcreateRef API の項

なお createRef() の導入後も 2 の callback を使った方法はサポートされるので、置き換える必要はない。


3. 予期せぬ副作用の検出

背景として、React の動作時には大きく2つのフェーズがある。

  • render フェーズ:DOM に適用する必要のある変更を決定する。 render メソッドが呼ばれ、結果を直前の render の結果と比較する
  • commit フェーズ:React がすべての変更を DOM に適用する。 componentDidMountcomponentDidUpdate などのライフサイクルメソッドもこのフェーズで呼ばれる

一般に commit フェーズは速いが render は遅い。そのため、非同期レンダリングによってレンダリング処理を複数の小さな処理に分割し、ブラウザをブロックしないように停止と再開をしながらレンダリングを行う。
これにより、commit 前に render フェーズのライフサイクルメソッドが複数回実行される可能性が生じる。
(このあたりはよくわかっていない)

ので、これらのライフサイクルメソッドに副作用がないことが重要となる。
これらのライフサイクルメソッドとは具体的には以下。

  • Class コンポーネントconstructor メソッド
  • render
  • setState の第一引数に関数を渡したときの関数(updater と呼ぶらしい)
  • getDerivedStateFromProps (v16.3.0 から componentWillReceiveProps の代替として追加)

ただ、これらのメソッドに副作用がないことを自動的に検出することは難しいため、StrictMode ではこれらのメソッドを2回ずつ実行する。

(サンプル)

class SideEffect extends Component {
  constructor(props) {
    super(props);

    this.state = { count: 0 };
    console.log('SideEffect constructor');
  }

  increment = () => {
    this.setState((prevState, props) => {
      console.log('SideEffect updater', prevState);
      return {
        count: prevState.count + 1,
      };
    });
  };

  render() {
    console.log('SideEffect render');
    return <div onClick={this.increment}>{this.state.count}</div>;
  }
}

export default function App() {
  return (
    <StrictMode>
      <SideEffect />
    </StrictMode>
  );
}

(結果)

f:id:dackdive:20180404023839p:plain


コード

一応上げておく。

https://github.com/zaki-yama/react-strict-mode-example