@mizumotokのブログ

テクノロジー、投資、書評、映画、筋トレなどについて

React HooksでクラスをStateless Functional Componentsで書き換えよう

React 16.8の新機能フック (hook)を使えば、状態を持つようなクラスもStateless Functional Components(SFC)で表現できるようになりました。
これからはどんどんSFCで書いていきましょう。
mizumotok.hatenablog.jp

f:id:mizumotok:20190323191009p:plain

ステート

クラスを使いたくなるのはほとんどがステートを使いたいからです。
SFCで書くにはuseStateフックを使います。

const [state, setState] = useState(initialState); 

クラス

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

SFC

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

componentDidMount / componentDidUpdate

componentDidMount と componentDidUpdateは同一の処理を書くことが多かったのですが、useEffectフックで一つにまとまりました。
useEffectはいくつも書くことができますので、今までは処理(副作用)がいくつあっても一つのライフサイクル関数に書いていましたが、副作用ごとに分けて書くことができます。

useEffect(didUpdate);

クラス

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

SFC

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

componentDidMountのみ

componentDidUpdateは必要ないときもあります。
その場合は第2引数に空配列[]を入れればいいです。

useEffect(() => {
  console.log('mounted!');
}, []);

componentWillUnmount

React Hooksではクリーンアップ関数と呼ばれます。
useEffectの第一引数の関数の戻り値がクリーンアップ関数になります。
副作用ごとに分けて書くことができるおかげで、副作用ごとにクリーンアップ関数を定義することになり、コードの見通しがよくなります。

クラス

componentDidMount() {
  this.subscription = props.source.subscribe();
}

componentDidUpdate() {
  this.subscription = props.source.subscribe();
}

componentWillUnmount() {
  this.subscription.unsubscribe();
}

SFC

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // Clean up the subscription
    subscription.unsubscribe();
  };
});

componentWillReceiveProps

変更前の値が不要

変更前の値が不要であれば、useEffectを使えます。
componentWillReceivePropsは監視対象の変数が変更したかどうかを確認する必要がありましたが、useEffectでは第2引数に指定した変数が変更したときのみ動くので、その確認する処理が不要になります。

クラス
componentWillReceiveProps(nextProps) {
  if (nextProps.count !== this.props.count) {
    console.log(`next count = ${nextProps.count}`);
  }
}
SFC
useEffect(() => {
  console.log(`next count = ${props.count}`);
}, [props.count])

変更前の値が必要

変更前の値はインスタンス変数に入れておく必要がありましたが、useRefフックでインスタンス変数を代替できます。

const refContainer = useRef(initialValue);

ステートの変更前の値をとっておきたい場合にも応用できます。

クラス
import React;

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.prevCount = 0;
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.count !== this.props.count) {
     this.prevCount = this.props.count;
  }

  render() {
    return (
      <div>
          <div>prev Count: {this.prevCount}</div>
          <div>current Count: {this.props.count}</div>
      </div>
    );
  }
}
SFC
import React, { useEffect, useRef } from 'react';

function Example(props) {
  const prevCountRef = useRef();

  useEffect(() => {
      prevCountRef.current = props.count;
  }, [props.count]);

  const prevCount = prevCountRef.current;

  return (
    <div>
      <div>prev Count: {prevCount}</div>
      <div>current Count: {props.count}</div>
    </div>
  );
}

shouldComponentUpdate

これはフックでは対応できません。React.memoを使って、コンポーネントまるごとメモ化しておく必要があります。

クラス

shouldComponentUpdate(nextProps) {
  return nextProps.count !== this.props.count
}

SFC

function _MyComponent(props) {
  ...
}

const MyComponent = React.memo(
    _MyComponent, 
    (prevProps, nextProps) => nextProps.count !== prevProps.count
);

useMemoで代替

そもそもshouldComponentUpdateのようなコンポーネント全体のレンダリングのパフォーマンス最適化は推奨されていなく、useMemoを使って個別の処理の最適化が推奨されています。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

DOM Refs

componentWillReceivePropsのときにも使用したuseRefフックが使えます。

const refContainer = useRef(initialValue);

クラス

import React from 'react';

class Example extends React.Component {
  constructor(props) {
    super(props);

    this.inputRef = React.createRef();
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

SFC

import React, { useEffect, useRef } from 'react';

function Example(props) {
  const inputRef = useRef();

  useEffect(() => {
    if (inputRef.current)
      inputRef.current.focus();
    }
  });

  return <input type="text" ref={inputRef} />;
}

インスタンス変数

this.myVar のようなインスタンスがメンバとして持っている変数ですが、これもuseRefフックで代替します。
useRefフックがインスタンス変数の代替で、DOM refsやcomponentWillReceivePropsに応用できるという方が正確でしょうか。

まとめ

React Hooksを使えば、ステートやライフサイクルメソッド、インスタンス変数といったクラスを使わなければならない理由だった要素を代替できます。
ほとんどのケースでクラスは不要になります。

React Hooksにはその他の機能や利点もあります。例えばuseContextフックではContextの値を取得できるので、Context.Consumerが不要になります。useEffectを応用してカスタムフックを作れば、HOC(High Order Components)の代替えにもなります。その場合、仮想DOMの階層を減らせるので開発中のDOMの確認が楽になります。
mizumotok.hatenablog.jp

作りながら学ぶ React入門

作りながら学ぶ React入門