【React】目的別hooks活用法【hooks】

2月の頭にReactのv16.8がリリースされ、`hooks`が正式に使えるようになってから半年が過ぎました。

非常に強力な`hooks`ですが、クラスコンポーネントに慣れた方の中には`hooks`を上手く使えないという方も多いのではないかと思います。
そこで今回は、クラスコンポーネントで行っていた**様々な処理**を`hooks`を用いて関数コンポーネントで実現するための解説を行おうと思います。

本記事は、React hooksを知っていて、API自体はなんとなくわかっていることを前提としています。
もし`hooks`が全くわからない場合は、[公式リファレンス](https://ja.reactjs.org/docs/hooks-reference.html)などを参照ください。

また、本記事に記載しているコードはReact v16.9.0とTypeScript v3.5.3[^1]で動作確認をしており、[GitHubでのコード公開](https://github.com/remew/react-hooks-example)と、[Netlifyでのホスティング](https://react-hooks-example.netlify.com)をしています。

## クラスコンポーネントでやっていたあれこれを置き換える

早速、クラスコンポーネントで行っていた様々な処理を`hooks`を使って関数コンポーネント(以下FC)に置き換えてみましょう。

### `componentDidMount`でAPIリクエスト

Reactのみでシンプルなアプリケーションを作る場合、`componentDidMount`でAPIリクエストを行うこともあるかと思います。<br>
そのような処理は、`useState`と`useEffect`を使って以下のように書くことができます。

要件
- コンポーネントマウント時にAPIリクエストを行う
- APIリクエスト中にはローディング表示を行う

```tsx:ComponentDidMount.tsx
import React, {useEffect, useState} from 'react';

type MaybeLoading<T> = {data: T, isLoading: false} | {data: null, isLoading: true};
type SomeState = {id: string, name: string};

// APIリクエストのダミー
const request: () => Promise<SomeState> = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve({id: '001', name: 'remew'}), 1000);
  });
};

const ComponentDidMount = () => {
  const [state, setState] = useState<MaybeLoading<SomeState>>({data: null, isLoading: true});
  useEffect(() => {
    request()
      .then(json => setState({data: json, isLoading: false}));
  }, []);
  if (state.isLoading) {
    return <p>Loading...</p>;
  }
  return (
    <>
      <p>id: {state.data.id}</p>
      <p>name: {state.data.name}</p>
    </>
  );
};

export default ComponentDidMount;
```

`useEffect`の第二引数に配列を指定すると、コンポーネントが再レンダリングされた際に配列の中身が変わっていないかを確認してもし変わっていれば第一引数に指定した関数が実行されます。
そのため、空配列を指定すると初回レンダリング時にのみ実行されるため、`componentDidMount`相当の処理をすることができます。

### `componentDidMount`/`componentDidUpdate`でAPIリクエスト

マウント時だけではなく、propsの特定の値が変わったときにAPIリクエストをしたいシチュエーションもあると思います。
これも同じく`useState`と`useEffect`を用いて実現することが可能です。
察しの良い方はわかると思いますが、`useEffect`の第二引数に監視するpropsの値を渡せば良いです。

```tsx:ComponentDidMountUpdate.tsx
import React, {useEffect, useState} from 'react';

type MaybeLoading<T> = {data: T, isLoading: false} | {data: null, isLoading: true};
type SomeState = {id: string, name: string};

// APIリクエストのダミー
const request: (id: string) => Promise<SomeState> = (id) => {
  return new Promise(resolve => {
    setTimeout(() => resolve({id, name: 'remew' + id}), 1000);
  });
};

// id指定用のラッパー
const Wrapper = () => {
  const [id, setId] = useState('');
  return (
    <>
      <label>id:<input onInput={(e: any) => setId(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
      <ComponentDidMountUpdate id={id} />
    </>
  );
};

const ComponentDidMountUpdate: React.FC<{id: string}> = props => {
  const [state, setState] = useState<MaybeLoading<SomeState>>({data: null, isLoading: true});
  useEffect(() => {
    if (!props.id) {
      return;
    }
    request(props.id)
      .then(json => setState({data: json, isLoading: false}));
  }, [props.id]);
  if (!props.id) {
    return <p>id is empty.</p>;
  }
  if (state.isLoading) {
    return <p>Loading...</p>;
  }
  return (
    <>
      <p>id: {state.data.id}</p>
      <p>name: {state.data.name}</p>
    </>
  );
};

export default Wrapper;
```

### `componentDidMount`と`componentWillUnmount`で処理を行う

例えば、1秒ごとに数値が加算されるようなコンポーネントを考えてみましょう。

クラスコンポーネントであれば、`componentDidMount`と`componentWillUnmount`を組み合わせて実現することが可能だと思います。
対してFCでは、`useEffect`を用いて以下のようなコードで実現することができます。

```jsx:TimerCountFC.js
import React, {useEffect, useState} from 'react';

const Timer = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timerId = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);

    return () => {
      console.log('clear interval'); // 確認用
      clearInterval(timerId);
    };
  }, []);

  return <div>{count}</div>;
};

export default Timer;
```

`useEffect`の第二引数に空配列を指定することで、`componentDidMount`相当の処理を行っています。
その中で`setInterval`を呼び出し、更にその中では`setCount`を呼び出しています。

`useEffect`の第一引数に渡した関数の返り値として返している関数は、**コンポーネントのアンマウント時**と、**第一引数に渡した関数が再実行される直前**に実行されます。
今回はコンポーネントがアンマウントされるときのみ実行されますが、第二引数に何らかの値を渡している場合、その値が変わって関数が再実行される際にも実行されます。

また、なぜ`setCount`にアロー関数を渡しているのかわからない方もいるかもしれませんが、本記事では深くは解説しません。
簡単に言うと、`setInterval`に渡した関数から見える`count`変数は初回レンダリング時の値(今回は初期値`0`)であるため、`setCount(count + 1)`としても毎回`1`がセットされてしまうためです。
詳しい説明は、[React公式が提供しているQ&A](https://ja.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function)をご覧下さい。

### `Context`を使用する

ReactのContext APIは、複数のContextをクラスコンポーネントで使用する際は、render propsパターンを用いる必要がありました。

`useContext`というhooksを用いることで、非常にシンプルにContextの値を使用することができます。

```tsx:UseContext.tsx
import React, {useContext, useState} from 'react';

const colorContext = React.createContext<string>('#000');

const Wrapper = () => {
  const [color, setColor] = useState('#000');
  return (
    <>
      <input value={color} type={'color'} onChange={e => setColor(e.target.value)} />
      <colorContext.Provider value={color}>
        <UseContext />
      </colorContext.Provider>
    </>
  );
};

const UseContext = () => {
  const color = useContext(colorContext);

  return <p style={{color}}>{color}</p>;
};

export default Wrapper;
```

`SomeContext.Provider`で値を渡す部分は変わりませんが、`<SomeContext.Consumer>{value => ...}</SomeContext.Consumer>`とする必要があった部分が非常にシンプルになっていることがわかると思います。

### `PureComponent`/`shouldComponentUpdate`

React製アプリケーションのパフォーマンスチューニングの手段として、`shouldComponentUpdate`を使用したり、`React.PureComponent`を継承している人も多いと思います。

FCでもpropsが変化しない場合の再レンダリングを抑制するためには、`React.memo`を使用する必要があります。
これは`hooks`ではありませんが、FCを利用する際に必要な知識のためついでに説明しておきます。

```tsx:ReactMemo.tsx
import React, {useContext, useEffect, useState} from 'react';

const SomeComponent: React.FC<{id: string, name: string}> = props => {
  console.log('re-rendering:', props.id);
  if (!props.name) {
    return <div>name is empty</div>;
  }
  return <div>name: {props.name}</div>;
};

const MemoizedSomeComponent = React.memo(SomeComponent);

const HighFrequencyUpdateComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count => count + 1);
    }, 100);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <label>name:<input onInput={(e: any) => setName(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
      <p>count: {count}</p>
      <SomeComponent id={'not-memoized'} name={name} />
      <MemoizedSomeComponent id={'memoized'} name={name} />
    </>
  );
};

export default HighFrequencyUpdateComponent;
```

上記コンポーネントを表示すると、コンソールに`re-rendering: not-memoized`という表示が連続して表示されますが、`re-rendering: memoized`は1度しか表示されないはずです(`name`のinputを変更すると再表示されます)。

`React.memo`に渡したコンポーネントは`PureComponent`を継承したコンポーネントのように、propsが変更されたときにのみ再レンダリングが実行されるようになります。

また、`React.memo`には第二引数として関数を渡すことができ、その関数が`true`を返した際にはレンダリングがスキップされます(`shouldComponentUpdate`と逆なので注意)。

```tsx:ReactMemoWithComparator.tsx
import React, {useContext, useEffect, useState} from 'react';

const SomeComponent: React.FC<{obj: {id: string, name: string}}> = props => {
  console.log('re-rendering:', props.obj.id);
  if (!props.obj.name) {
    return <div>name is empty</div>;
  }
  return <div>name: {props.obj.name}</div>;
};
const WrongMemoizedSomeComponent = React.memo(SomeComponent);
const MemoizedSomeComponent = React.memo(SomeComponent, (oldProps, newProps) => {
  return oldProps.obj.id === newProps.obj.id && oldProps.obj.name === newProps.obj.name;
});

const HighFrequencyUpdateComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count => count + 1);
    }, 500);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <label>name:<input onInput={(e: any) => setName(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
      <p>count: {count}</p>
      <SomeComponent obj={{id: 'not-memoized', name}} />
      <WrongMemoizedSomeComponent obj={{id: 'wrong-memoized', name}} />
      <MemoizedSomeComponent obj={{id: 'memoized', name}} />
    </>
  );
};

export default HighFrequencyUpdateComponent;
```

先程の例と違い、`props`の型を`{obj: {id: string, name: string}}`というように階層構造にしています。

`WrongMemoizedSomeComponent`は、単純に`React.memo`でラップしているだけですが、`props.obj`は毎回違うオブジェクトの参照になっているためレンダリングの抑制をすることができていません。

`MemoizedSomeComponent`では`React.memo`の第二引数に比較関数を渡しています。
`props.obj.id`と`props.obj.name`をしっかり比較しているため、レンダリングの抑制ができています。

## さいごに

以上で目的別`hooks`の活用法の紹介を終わります。

他にも思いついたら追記したいと思います。

`hooks`が登場した当初は「関数コンポーネントに状態が持てる！」「末端の関数コンポーネントで状態が必要になったときに修正が少なくて済む」などのような意見が多かったように思います。

しかし個人的には、`hooks`は**関数コンポーネントに状態を持たせる**ためだけに使うだけではなく、**クラスコンポーネントを関数コンポーネントに置き換える**ために使うことでより真価を発揮する機能だと思っています(あくまで新規コンポーネントを作成するときや、既存コンポーネントを修正するときの話です[^2])。

`hooks`を駆使してはじめから関数コンポーネントベースで開発をすることで、開発効率が向上するのではないかと考えています。

みなさんが良い`hooks`生活を送れることを祈っています。

## 参考リンク

- https://ja.reactjs.org/docs/react-api.html#hooks
- https://ja.reactjs.org/docs/hooks-faq.html
- https://qiita.com/uhyo/items/246fb1f30acfeb7699da

[^1]: TypeScriptを使っているのは、TypeScriptと絡めた説明もするつもりだったためです。
[^2]: Reactの公式ドキュメントでは[クラスコンポーネントを全部書き換える必要はない](https://ja.reactjs.org/docs/hooks-faq.html#do-i-need-to-rewrite-all-my-class-components)と言われており、[既存のクラスをフックに書き換えることも推奨されていません](https://ja.reactjs.org/docs/hooks-faq.html#should-i-use-hooks-classes-or-a-mix-of-both)。