React v16.6

10月24日にReact V16.6がやってきました。いくつかの機能の追加、及び廃止APIの警告が行われています。今回は追加された機能のうち、以下の3つに触れてみます。

リリース直後のため、若干自信のない部分もあります。あしからず。

React.lazy()

遅延ロードのサポートです。これからはプラグインを入れたり自前で書く必要はなくなります。遅延ロードってなんぞという方は過去記事をどうぞ。

使い方はlazyをimportしてコンポーネントを読み込むだけです。SuspenseというAPIにfallback属性を与えること、読み込み完了までのフォールバック処理も指定できます。

import { lazy } from 'react';
const LazyLoadedComponent = lazy(() => import('./LazyComponent));

<Suspense fallback={<div>Loading Now...</div>}>
	<LazaLoadedComponent />
</Suspense>

React Routerでルーティング先に合わせてコンポーネントを読み込む例です。

import React, { Suspense, lazy } from "react";
import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom";

const Home = lazy(() =>
  import("./components/Home" /* webpackChunkName: "Home" */)
);
const About = lazy(() =>
  import("./components/About" /*webpackChunkName: "About" */)
);

const App = () => (
  <Router>
    <div>
      <h1>Lazy Test</h1>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
      <Suspense fallback={<p>Loading...</p>}>
        <Switch>
          <Route exact path="/" component={() => <Home />} />
          <Route path="/about" component={() => <About />} />
        </Switch>
      </Suspense>
    </div>
  </Router>
);

export default App;

Routeのcomponent属性はcomponent={Home}のように直接コンポーネントを指定しても動作しますが、ビルドで警告が出るため上記の表記をしています。同じく、<Suspense><Router>直下で丸ごとラップすると動作しなかったため、<Route>のみにラップしています。

ChunkNameにも対応しており、react-loadableを利用する場合とほぼ同様に書き出せました。


                        Asset       Size  Chunks             Chunk Names
About.db20411b492caa7aca73.js   2.34 KiB   About  [emitted]  About
 Home.db20411b492caa7aca73.js   2.27 KiB    Home  [emitted]  Home
 main.db20411b492caa7aca73.js   3.36 MiB    main  [emitted]  main
                  favicon.ico   3.78 KiB          [emitted]
                 ./index.html  266 bytes          [emitted]
Entrypoint main = main.db20411b492caa7aca73.js

React.memo()

関数コンポーネント版のReact.PureComponentです。PureComponentに関しては、こちらの記事で説明しています。

関数コンポーネントにはライフサイクルメソッドがないため、shouldComponentUpdateでレンダリング条件などを変更することができません。React.memoは、関数コンポーネントが受け取るpropsに変化があった場合にのみ、再レンダリングを行うようにしてくれるHOCです。

const Component = React.memo(function(props) {});

ラップされたコンポーネントはpropsを浅い比較で変化の判断をしますが、より細かく設定したい場合は第2引数に比較用の関数を渡して挙動を変更できるようです。

以下は必要部分のみ再レンダリングが行われるかを確認する例です。Appコンポーネントのmsg stateをループさせ、その数の分だけMsgコンポーネントでレンダリングします。

import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  state = { msg: ["first Message"] };

  addMsg = e => {
    const msg = [...this.state.msg];
    msg.push("New Msg");
    this.setState({ msg });
  };

  render() {
    return (
      <div>
        <button onClick={e => this.addMsg(e)}>Add</button>
        <ul>{this.state.msg.map((m, i) => <Msg key={i} msg={m} />)}</ul>
      </div>
    );
  }
}

const Msg = props => {
  console.log("add");
  return <li>{props.msg}</li>;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

これをボタンクリックすると、新しく追加されたmsgが画面に追加されていきます。

一見うまく動作しているように思えますが、コンソールを見るとクリックのたびにmsg配列すべての要素分がループされているのが確認できます。わずか数十クリックで1000近い画面レンダリングが行われています。

React Developer ToolのHighlight Updatesで確認しても、追加分だけでなく全件分が再レンダリングされているのが見て取れます。変化していないデータまで更新するのは無駄ですし、パフォーマンスにも影響するでしょう。

これを改善するためにReact.memoを使います。HOCなので利用は簡単です。

import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
  state = { msg: ["first Message"] };

  addMsg = e => {
    const msg = [...this.state.msg];
    msg.push("New Msg");
    this.setState({ msg });
  };

  render() {
    return (
      <div>
        <button onClick={e => this.addMsg(e)}>Add</button>
        <ul>{this.state.msg.map((m, i) => <PureMsg key={i} msg={m} />)}</ul>
      </div>
    );
  }
}

const Msg = props => {
  console.log("add");
  return <li>{props.msg}</li>;
};

// 追加したコード(render内のコンポーネント名も変更しておきます)
const PureMsg = React.memo(Msg);

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

これで追加分だけがレンダリングされるようになります。React.memoによりpropsが変化したと判断されたときにのみレンダリングが行われるからです。画面上の結果は同じですが、コンソール結果は異なります。

React Developer Highlight Updatesでも確認してみます。

React.memoはPureComponent同様、浅い比較を使うため、ネストされたオブジェクトの場合は注意が必要です。Immutableなオブジェクトで解決するか、React.memo自体の使用を考え直す必要があるでしょう。

static contextType

クラスコンポーネントでサポートされた新しいcontextTypeというプロパティは、this.contextを介してReact.createContext()で作成した値にアクセスできます。取得できる値は、その地点の最新の値を受け取れます。ライフサイクルメソッド内でも参照可能。

現行のContext APIに関しては過去記事で解説しています。

const LocaleContext = React.createContext('blue');

class LocalizedComponent extends React.Component {
  static contextType = LocaleContext;

  render() {
    return <div>The locale is: {this.context}</div>
  }
}

// LocalizedComponent.contextType = LocaleContextと同義

通常、ContextAPIのProviderでラップした値はConsumerで受け取りますが、このthis.contextを使えばConsumerの外からでもコンテキストの値にアクセスできるようです。公式のサンプルコードで、Consumerを記述していないThemeButton内でthis.contextを使いコンテキスト値にアクセスできました。

import { ThemeContext } from "./theme-context";
import React from "react";

class ThemedButton extends React.Component {
    static contextType = ThemeContext;
	render() {
    	console.log(this.context); // 最新のコンテキスト値にアクセス可能
        return (...);
	}
}

以前の記事で作成したカウンターを修正し、Consumerで受け取ったコンテキスト値とstaticで取得したコンテキスト値を表示する例です。

Demo

class C extends React.Component {
  static contextType = CountContext;

  render() {
    let staticCount = this.context.count;
    return (
      <CountContext.Consumer>
        {context => {
          return (
            <div>
              { /* staticとconsumerから受け取った値を表示 */ }
              <h2>static ver: {staticCount}</h2>
              <h2>consumer ver: {context.count}</h2>
              <button onClick={context.actions.onIncrement}>+</button>
              <button onClick={context.actions.onDecrement}>-</button>
            </div>
          );
        }}
      </CountContext.Consumer>
    );
  }
}

export default C;

どちらの値も同じ値にアクセスしているので、表示される値は同じです。

 

React 16.6ではレガシーなContextAPIを使用すると警告が表示されます。開発側は旧API利用者を新しいContextAPIにさっさと移行させたいようで、今後も機能追加が見込まれています。

おわりに

v16.6では他にもgetDerivedStateFromErrorというライフサイクルメソッドの追加や、ReactDom.findDOMNode()などのメソッドの廃止が行われています。すべての変更点は公式にて。

すでにv16.7の話も出ていて、前々から検討されていた関数コンポーネントに状態を持たすことが可能になる模様。これによりrecomposeを使うようなコンポーネント設計が可能になるかもしれません。