@mizumotokのブログ

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

React Hooksが面白い

React 16.8から新機能フック (hook)が追加されました。ステートやcomponentDidMount等のでライフサイクルメソッドを使いたい場合でも、クラスを使わないStateless functional component(SFC)で表現できるようになりました。

f:id:mizumotok:20190323191009p:plain

ステートフック

クラスを使う理由の一つがステートの管理をしたい場合です。
今までだとカウンタをステートで管理するとこのように書いていました。

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>
    );
  }
}

これが以下のようになります。

import React, { useState } from 'react';

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

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

コードがシンプルになりました。
useStateという関数がステート変数とステート更新関数を作ってくれます。
引数に初期値を渡すことで、初期化までやってくれてしまいます。

Stateless functional component(SFC)なのでテストも書きやすいですね。

副作用フック

関数というのは入力があって出力があります。副作用というのは出力以外の状態に影響を与えることです。Reactの場合、propsが入力で出力(戻り値)はReact Componentになりますが、React Componentを作成する以外に、ライフサイクルメソッドでステートを変更したり、その他の影響を促すことがあります。これが副作用です。
副作用フック(useEffect)はcomponentDidMount と componentDidUpdate と componentWillUnmount がまとまったものだと考えることができます。


以下の例では、React Componentを作成する以外の影響、document.titleの変更を副作用フックで行っています。

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には同じ処理を書くことが多かったですが、一つにまとまってすっきりしました。
副作用は複数あることもあり、今まではcomponentDidUpdateにまとめなかればなかったのが、副作用の数だけuseEffectを呼び出せばよく見通しが良くなります。

また、useEffect に渡す関数はクリーンアップ用関数を返すことができます。クリーンアップ用関数とはアンマウント時に実行される関数で、React.ComponetクラスにおけるcomponentWillMountに相当します。

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

useEffectの第2引数に、この副作用が依存している値の配列を渡すことができます。

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

クラスではcomponentWillReceivePropsでどのpropsが変わったかの場合分けがたくさん出てきていましたが、いちいち場合分けしなくてよくなりました。これは嬉しい。

空の配列 [] を渡すと、この副作用がコンポーネント内のどの値にも依存していないということを React に伝えることになります。つまり副作用はマウント時に実行されアンマウント時にクリーンアップされますが、更新時には実行されないようになります。componentDidMount を componentDidUpdate区別した副作用に使います。

カスタムフック

独自のフックを作れます。

以下の例では、friendの状態に応じて、OnlineかOfflineか返します。

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

friendの状態に応じて表示を変えたいユースケースってあちこちで出てきますよね。useEffect内の関数をコピペするのは 決していい手段とは言えません。

以下のようにuseEffectだけを切り出します。useから始まる関数名にするのが推奨されています。

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

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;

先ほどのFriendStatusは以下のようになります。描画だけに集中できて見通しがよくなりましたし、useFriendStatusは使い回しができます。

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

これは今までのReact.ComponentではHOC(High Order Components)という手法を使っていました。

import React rom 'react';

function withFriendStatus(component) {
    class WithFriendStatus(props) extends React.Component {
        constructor(props) {
            super(props);
            this.state = { isOnline: null }
            this.handleStatusChange = this.handleStatusChange.bind(this);
        } 

        componentDidMount() {
            ChatAPI.subscribeToFriendStatus(this.props.friendID, this.handleStatusChange);
        }

        componentDidUpdate() {
            ChatAPI.subscribeToFriendStatus(this.props.friendID, this.handleStatusChange);
        }

        componentWillUnMount() {
            ChatAPI.unsubscribeFromFriendStatus(this.props.friendID, this.handleStatusChange);
        }

        handleStatusChange(status) {
            this.setStatus( { isOnline: status.isOnline });
        }

        render() {
            return <component {...props} isOnline={this.state.isOnline} />;
        }
    }
    return WithFriendStatus;
}
import React rom 'react';

function FriendStatus(props) {
  const { isOnline } = props;

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

export default withFriendStatus(FriendStatus);

HOCを使うとReact DevToolsで見たときに、階層が増えてしまいます。カスタムフックを使えばHOCやらプロバイダやらコンシューマやら無駄な階層を作らずに、すっきりとした仮想DOMを保つことができます。

その他のフック

useContext

コンテキストもフックで表現できます。
Providerは作らないといけませんが、ConsumerでProviderの値を取る代わりに、useContextから値を取ることができます。

useReducer

useStateは単純にステートの値を更新するだけでしたが、useReducerではreducerを使ってもう少し複雑な処理ができます。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter({initialState}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

reducerについては、reduxと同じですね。
reduxはストアを一箇所(Single source of truth)にするのが基本ですが、useRedcerはReact.Componentごとに使います。

useRef

React.createRefの代替です。

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useMemo

複雑な計算を毎回しなくていいように、メモ化しておきます。

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

useEffectのように第2引数が変更されたときに実行されます。パフォーマンス最適化のために使うもので、React.MemoやshouldUpdateComponentの代替にもなります。
useMemoはレンダー中に実行し、レンダー以外の副作用にはuseEffectを使います。

まとめ

  • ほとんどのコンポーネントをクラスを使わずにStateless functional component(SFC)で表現できるようになります。
  • 副作用を分離しやすくなり、ソースコードの見通しがよくなります。
  • HOC(High Order Components)やContextを使ったときに起きる仮想DOMの階層化を避けることができます。

実際に使ってみるとソースコードが理解しやすくなってとても気持ちがいいです。クラスは便利すぎて副作用を書きやすく、どんどん複雑化しがちですからね。
あと、SFCで書き始めて状態管理が必要になって途中でクラスに書き換えことはよくありますが、その必要がなくなるのは地味に嬉しい。

作りながら学ぶ React入門

作りながら学ぶ React入門