安全に React Hooks を使用する

React Hooks はとても便利で、Custom Hooks を上手く実装することで、再利用性の高いコードを、簡潔に実装することができます。

しかし、Hooksを不用意に使用してしまうと **意図しない無限ループに陥ったり、正しくStateが反映されなかったりすること**があります。

## useCallback を使い無限ループを避ける

例えば以下のようなDivの大きさを取得する`useRect`というCustom Hooksについて考えてみます。

```ts
const useRect = () => {
  const [rect, setRect] = useState<ClientRect | DOMRect>();
  const ref = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const target = ref.current;
    if (target) {
      const rect = target.getBoundingClientRect();
      setRect(rect);
    }
  }, []);

  return { ref, rect };
};
```

このHooksは以下のようにrefをセットして使用できます。

```tsx
const App: React.FC = () => {
  const { ref, rect } = useRect();

  return (
    <div className="target" ref={ref}>
      <p>Width: {rect ? `${rect.width}px` : "undefined"}</p>
      <p>Height: {rect ? `${rect.height}px` : "undefined"}</p>
    </div>
  );
};
```

ここで、windowの大きさが変化した時に、divの大きさを取得し直したいと考えたとします。この場合は、handleResizeのようなhandlerを記述し、EventListenerのresizeイベントに登録することになるでしょう。

```ts
import debounce from "lodash/debounce";

export const useRect = () => {
  const [rect, setRect] = useState<ClientRect | DOMRect>();
  const ref = useRef<HTMLDivElement | null>(null);

  const resize = () => {
    if (ref.current) {
      const rect = ref.current.getBoundingClientRect();
      setRect(rect);
    }
  };
  useEffect(() => resize(), []);

  const handleResize = debounce(resize, 16);
  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, [handleResize]);

  return { ref, rect };
};
```


このHooksは一見正しく動作するように見えます。しかし、`resize`関数は、描画のたびに毎回生成されてしまいます。ここで問題になるのは、**`resize`を呼び出すと`resize`関数が新たに生成されてしまう**、ということです。

例えば、うっかり、mount時に`resize`を呼び出している useEffect を誤って以下のように書き換えてしまうとどうなるでしょうか？

```ts
 useEffect(() => resize(), [resize]);
```

この場合は、resize を呼び出すと resize が生成され また useEffect が実行されてしまい、無限に処理が実行されてしまいます。

ここで、  `eslint` の `react-hooks/exhaustive-deps` を入れている場合は、次のような warning を出してくれます。

```
./src/useRect.ts
  Line 8:  The 'resize' function makes the dependencies of useEffect Hook (at line 18) change on every render. To fix this, wrap the 'resize' definition into its own useCallback() Hook  react-hooks/exhaustive-deps
```

この warning に書かれている通り、この無限ループを避けるためには、`useCallback` で `resize` を囲むことで、描画ごとにresizeが生成されることを避ける必要があります。

```ts
const resize = useCallback(() => {
  if (ref.current) {
    const rect = ref.current.getBoundingClientRect();
    setRect(rect);
  }
}, []);
```

`useCallback` の代わりに `useMemo` を使っても良いでしょう。

```ts
const resize = useMemo(() => {
  return () => {
    if (ref.current) {
      const rect = ref.current.getBoundingClientRect();
      setRect(rect);
    }
  }
}, []);
```

適切にメモ化をすることで、描画ごとにresizeは生成されることはなくなり、無限ループを回避できるようになりました！

## `react-hooks/exhaustive-deps` を設定する
`useCallback` による関数のメモ化ができていても、第二引数が正しく設定されていないと、`state` の更新がうまくいかない場合があります。

例えば以下のような簡単な counter を考えます。

```tsx
const App: React.FC = () => {
  const [count, setCount] = useState(0);

  const addCount = useCallback(() => {
    setCount(count + 1);
  }, []);

  return (
    <div className="target">
      <button onClick={() => addCount()}>Add Count</button>
      <p>count: {count}</p>
    </div>
  );
};
```

この例では、`addCount` が 描画の最初にメモ化されていますが、メモ化されているせいで`count` の参照が古いままになっています。この例では、`useCallback` には `count` を第二引数に加える必要があります。

```ts
const addCount = useCallback(() => {
  setCount(count + 1);
}, [count]);
```

 `react-hooks/exhaustive-deps` を lint の rule として設定していると、この場合も以下のように warning を出してくれます。

```
./src/App.tsx
  Line 9:  React Hook useCallback has a missing dependency: 'count'. Either include it or remove the dependency array. You can also do a functional update 'setCount(c => ...)' if you only need 'count' in the 'setCount' call  react-hooks/exhaustive-deps
```

 `react-hooks/exhaustive-deps`は、Hooks内で参照している変数が第二引数に加えられていない場合に warning を出します。この lint を入れておくことで、古い変数を参照してしまうケースを避けることができます。

さらに、この`react-hooks/exhaustive-deps` は賢く、`useState` の dispatcher や、`useRef` の current など設定の必要がないものは除外してくれます。

また、 `react-hooks/exhaustive-deps` を設定しておく副次的なメリットとして、仮に第二引数を意図的に変更する場合も、コメントでdisableにする必要があるということが挙げられます。

例えば、Mount時にだけ呼び出したい場合や、使用しているライブラリが毎描画ごとに変更される場合など、第二引数を意図的に変えたい時、自分たちのチームでは、なぜ `react-hooks/exhaustive-deps` を disable するかのコメントを eslint-disable のコメントとともに付記しています。

このルールによって、Hooksの引数を誤って変更してしまうリストを減らすことができました。

```ts
useEffect(() => {
// ...
// apolloClient は毎描画ごとに変更されてしまうため第二引数から除外する
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paramId]);
```

React の公式ドキュメントでも、`react-hooks/exhaustive-deps` を設定することをお勧めしています。最新の`create-react-app` では、デフォルトでこのルールが設定されています。

> We recommend using the [exhaustive-deps](https://github.com/facebook/react/issues/14920) rule as part of our [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks#installation) package. It warns when dependencies are specified incorrectly and suggests a fix.
cf. [Hooks API Reference – React](https://reactjs.org/docs/hooks-reference.html#useeffect)


## stop-runaway-react-effects
 `react-hooks/exhaustive-deps` によるルールチェックは万能ではなく、間接的に実行する関数が変化する場合をチェックできません。例えば、`useRect` の例にある、 `handleResize` を使用している `useEffect` の処理を以下のように書き換えた場合、

```ts
useEffect(() => {
  handleResize();
  window.addEventListener("resize", handleResize);
  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, [handleResize]);
```

この場合も `handleResize` が `resize` を呼び出すため、`useCallback` でメモ化されていない場合は、無限に処理が走ってしまいますが、`react-hooks/exhaustive-deps` は warning を出してくれません。

そこで、自分たちのチームは [stop-runaway-react-effects](https://github.com/kentcdodds/stop-runaway-react-effects) を使用して、 useEffect の無限ループがないか監視しています。

このライブラリは `useEffect` や `useLayoutEffect` の 処理を tracking して、ある一定の期間で `useEffect` の実行回数が閾値を超えた場合に warning を表示してくれます。また、その warning で第二引数を表示してくれるので、**どこの引数が変更されてしまっているか**を確認することができます。

導入は簡単で、index に以下のコードを追加するだけです。時間当たりの閾値を設定することもできます。

```tsx
import { hijackEffects } from "stop-runaway-react-effects";

if (process.env.NODE_ENV !== "production") {
  hijackEffects();
  // hijackEffects({ callCount: 10, timeLimit: 1000 });
}
```

無限ループが起きている場合には、以下のようにwarningが出ます。

<img alt="Screen Shot 2019-05-26 at 20.12.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150677/89b2bd77-f82e-3721-3bdb-164e36697756.png">


## まとめ

React Hooksの引数の設定・メモ化を適切に行うことで、より安全にHooksを使用することができます。具体的には、以下の3つに留意しましょう。

- 適切にメモ化を行い無限ループを避ける
- 適切なメモ化を行うために`react-hooks/exhaustive-deps`を設定する
- `stop-runaway-react-effects` で無限ループを監視する（お好み）

冒頭にも書きましたが、Hooksを使用することで、再利用性の高いコードを、簡潔に実装することができます。これは、可読性やパフォーマンスの向上にも繋がります。ぜひ安全に、そして積極的にHooksを使っていきましょう！