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
です。
関数コンポーネントにはライフサイクルメソッドがないため、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()
で作成した値にアクセスできます。取得できる値は、その時点の最新の値を受け取れます。ライフサイクルメソッド内でも参照可能。
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で取得したコンテキスト値を表示する例です。
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を使うようなコンポーネント設計が可能になるかもしれません。