コンパイラかく語りき

import { Fun } from 'programming'

React v16.6.0で追加された lazy, memo そして contextType を試してみる

React.js v16.6.0 にて追加された新機能を試してみました。

reactjs.org

環境づくり

サクッと、create-react-app を利用しました。

create-react-app - github.com

$ npm -g install create-react-app

$ npx create-react-app my-app

$ cd my-app

$ yarn start

初めて create-react-app を利用したのですが、簡単すぎてビビりました…。React で何か試したい時は積極的に使っていきたいですね。

React.memo

プレゼンテーションコンポーネント(独自に state を保持したり、ライフサイクル関数を使うことなく、描画の行うだけのコンポーネント)を定義するする時は、クラスコンポーネントではなく SFC を利用しています。

import React from 'react';

// ふつーの SFC
const Hello = () => {
  return (
    <div>Hello React v16.6!</div>
  )
}

しかし、最近業務で引っかかった問題として、SFC で不要な再レンダリングが起こっていました。

レンダリングの原因は、空配列であった state に空配列を再代入していたことでした。値は変わっていないのに、参照が変わっていたため、SFC が props に更新ありと判定していました。

参考リンク: optimizing-performance - reactjs.org

そこで、PureComponent への書き換えを行いました。

PureComponent は props を shallow 比較するのでパフォーマンス向上に寄与しました。

React.memo を使うと、SFC を PureComponent に書き換える事なく同様のことが実現できます。

参考リンク: React.memo - reactjs.org

import React from 'react';

// ふつーの SFC
const Hello = (props) => {
  return (
    <div>Hello React v16.6!</div>
  )
}

// props を shallow 比較するコンポーネント
const PureHello = React.memo(Hello)

クラスへの書き換えの手間がかからなくて良さげです。

React.Suspense と React.lazy

ここで、API フェッチを行い、そして待ち合わせを行うようなコンポーネントのケースを考えてみます。 従来ならこのように書いていました。

import React, { Component } from 'react';

// 与えられた Todo を表示する
const Todo = (props) => {
  if(props.isLoading) {
    return <div>Loading...</div>
  }
  return (
    <div>{ props.todo.title }</div>
  )
}

// Todo のフェッチを行い、フェッチ状況も管理する
class App extends Component {
  state = {
    isLoading: false,
    todo: null
  }

  componentDidMount() {
    this.setState({ isLoading: true })

    fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(res => res.json())
      .then(json => {
        this.setState({ isLoading: false, todo: json })
      })
  }
  
  render() {
    return (
        <Todo isLoading={this.state.isLoading} todo={this.state.todo} />
    );
  }
}

React.Suspense を使って書くと以下のようになります。(多分、細部では違った処理になっているかもしれません)

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

const Todo = (props) => {
  if(todo === null) {
    throw new Promise((resolve, reject) => {
      fetch(`https://jsonplaceholder.typicode.com/todos/${props.id}`)
        .then(res => res.json())
        .then(json => {
          todo = json
          resolve()
        })
        .catch(e => {
          console.warn("Not fetched yet")
          reject()
        })
    })
  }

  return (
    <div>{ todo.title }</div>
  )
}

class App extends Component {
  render() {
    return (
      {/* Suspense にフェッチ完了までのリンダリングを引き受けさせる*/}
      <Suspense fallback={<div>Loading...</div>}>
        <Todos id={1} />
      </Suspense>
    );
  }
}

参考URL: React 16.6で追加されたReact.Suspenseについて - qiita.com

さらに、この Suspense ですが、React.lazy を使った dynamic import によって Code splitting を噛ませることもできるようです。

import React, { Component, Suspense, lazy } from 'react';

const Todo = lazy(() => import('path/to/Todo.jsx'))

class App extends Component {
  render() {
    return (
      <Suspense fallback={<div>Loading...</div>}>
        <Todo id={1} />
      </Suspense>
    );
  }
}

export default App;

SSRではまだ使えないようですが…。

参考URL:

static contextType

Context は以前から実装されていた機能ですが(実はあんまり使ったこと無い)、 クラスの static 変数として context にアクセスできるようになったみたいです。

import React, { Component } from 'react';

const MyContext = React.createContext("default value");

class MyClass extends React.Component {
  static contextType = MyContext;

  render() {
    console.log(this.context) // "default value"
    return (
      <div>{ this.context }</div>
    )
  }
}

参考URL: static contextType - reactjs.org

static getDerivedStateFromError()

従来の componentDidCatch に加えて、 getDerivedStateFromError が追加されました。

componentDidCatch の問題点は、SSR に対応していないことでした。 getDerivedStateFromError は将来的には SSR 対応されるようなので、ユニバーサルなアプリケーションには役立ちそうです。

参考URL: static getDerivedStateFromError()

以上、 React v16.6 で追加された機能について試したり、調べたりしてみました。