Reactを再考する#3

## これまで

[第一回](https://qiita.com/a2-tsuchiya/items/7a827b0b5487c9db2b75): ```useState```と```useRef```
[第二回](https://qiita.com/a2-tsuchiya/items/fe5260ae2ea8de67cb77): ```useContext```

に続く、Reactを掘り下げてみようシリーズです。
今回は、```useEffect```をメインに行こうと思います。


## 前提

- React 16.8以降。なので**Hook API**の使用が前提です
- Reactの基本的な説明はしません
- TypeScriptで書いてます

## 一応説明する

- ```useEffect```
    - コンポーネントの描画が完了した後に、副作用として呼び出されるフック
    - 副作用とは、コンポーネントの描画**以外**の処理を指す


## 使ってみる

描画時に、データ（ブログ記事など）を取得して画面に表示させます。

**./src/App.tsx**

```tsx
import React, { useState, useEffect } from "react";
import postData from "./post-data.json";

interface IPost {
  id: string;
  title: string;
  body: string;
  date: string;
}

/** 外部APIのダミー */
const dammyAPI = async () => {
  const newPosts = postData
  return Promise.resolve(newPosts)
}

/** useEffectを実装したカスタムフック */
const usePosts = () => {
  const [posts, setPosts] = useState([] as IPost[]);
  const addPost = (newPosts: IPost[]) =>
    setPosts((allPosts) => [...newPosts, ...allPosts]);

  useEffect(() => {
    dammyAPI()
      .then(newPosts => addPost(newPosts))
      .catch(err => console.error(err))
    return () => console.log("good bye")
  }, []);  

  return posts
}

export default function App() {
  const posts = usePosts()
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}
```

肝心な部分はコチラ

```tsx
/** ココが描画後に実行される */
useEffect(() => {
  dammyAPI()
    .then(newPosts => addPost(newPosts))
    .catch(err => console.error(err))
  return () => console.log("good bye") //①
}, []);   //②
```

- ①　コールバック関数の戻り値を設定すると、アンマウント時に実行される
- ②　**依存配列**により副作用が実行される条件を指定できる（空の場合は初回のみ実行される）
    - 変数を指定すると、その変数が更新されたタイミングで副作用が実行される


### 依存配列の同一性チェック

要は*JavaScript*さんが何をもって同じ（もしくは変更された）と判断するか、です。
検証のため、キーボード入力のたびに再描画されるコンポーネントを用意します。

```tsx
export const useAnyKeyToRender = () => {
  const [, setKey] = useState("");
  const handler = (e: KeyboardEvent) => setKey(e.key);

  useEffect(() => {
    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, []);
};
```

**依存配列を指定しない場合**
当然ですが、キーボード入力するたびにコンソールに出力（再描画）されますね。

```tsx
export default function App() {
  useAnyKeyToRender();
  useEffect(() => {
    console.log("fresh render");
  });
  return <h1>Hello, React.js</h1>;
}
```

**依存配列にプリミティブ値を指定した場合**
コンソールには一回しか出力（再描画）されません。
これは```word```変数の内容が変わらないからですね（**再代入されたとしても**）

```tsx
export default function App() {
  useAnyKeyToRender();
  const word = "abc"
  useEffect(() => {
    console.log("fresh render");
  }, [word]);
  return <h1>Hello, React.js</h1>;
}
```

**依存配列に配列を指定した場合**
あれ、キーボード入力するたびにコンソールに出力（再描画）されますね。

- 配列（やオブジェクト、関数）などは参照値である
- 参照値は、データの内容ではなく同一のインスタンスかどうかで判断する
- コンポーネントが描画される度に、新しい配列のインスタンスが生成されている
- 都度再描画される

```tsx
export default function App() {
  useAnyKeyToRender();
  const words = ["a", "b", "c"]
  useEffect(() => {
    console.log("fresh render");
  }, [words]);
  return <h1>Hello, React.js</h1>;
}
```
これはパフォーマンスに大きく影響しそうですね。。
[ESLint](https://ja.reactjs.org/docs/hooks-rules.html)で設定していれば警告してくれます。(たしか*eslint-plugin-react-hooks*)

**回避策**
同じインスタンスを参照すればいいんだから、関数外で定義すればいいんじゃね？

```tsx
const words = ["a", "b", "c"]

export default function App() {
  useAnyKeyToRender();
  useEffect(() => {
    console.log("fresh render");
  }, [words]);
  return <h1>Hello, React.js</h1>;
}
```

正解（の一つ）です。
では、関数外で定義できない時（コンポーネントのプロパティ値を使わないといけない時など）
はどうすればいいでしょうか。。

### useMemoを使う

- 戻り値をキャッシュして、**メモ化**する
- 参照するインスタンスが同一であることを保証される

ということで、コンポーネントのプロパティ値を使う場合を考えてみます。

**./src/WordCount.tsx**

```tsx
import React, { useEffect, useMemo } from "react";
import { useAnyKeyToRender } from "./App";

const WordCount: React.FC = ({ children = "" }) => {
  useAnyKeyToRender();

  const words = useMemo(() => {
    return String(children).split(" ");
  }, [children]);

  useEffect(() => {
    console.log("fresh render");
  }, [words]);

  return (
    <>
      <p>{String(children)}</p>
      <p>
        <strong>{words.length} - words</strong>
      </p>
    </>
  );
};
export default WordCount;
```

**./src/App.tsx**

```tsx
import WordCount from "./WordCount"

export default function App() {
  useAnyKeyToRender();
  return <WordCount>Hello, React.js</WordCount>;
}
```

はい、コンソールには一回しか出力（再描画）されません。
描画が遅いな、、と思ったら試してみるといいかもしれません。。

### useCallback

似たような機能があるのでついでに。。
```useMemo```はメモ化された値を返すのに対して、```useCallback```はメモ化された関数を返します。

```tsx
import React, { useEffect, useCallback } from "react";

export const HelloWorld = () => {
  useAnyKeyToRender();

  const fn = useCallback(() => {
    console.log("hello")
    console.log("world")
  }, [])

  useEffect(() => {
    console.log("fresh render");
    fn();
  }, [fn]);
  return <h1>Hello, world</h1>;
};

export default function App() {
  return <HelloWorld />;
}
```

### useLayoutEffect

ついでのついでですね。。呼ばれるタイミングの違う```useEffect```という感じです。
コンポーネントの描画サイクルを見れば一目瞭然だと思います。

1. コンポーネントの描画関数が呼び出される
1. ```useLayoutEffect```で設定した副作用関数が呼び出される
1. ブラウザのPaint処理によりコンポーネントの描画結果が画面に反映される
1. ```useEffect```で設定した副作用関数が呼び出される

```tsx
import React, { useEffect, useLayoutEffect } from "react";

export default function App() {
  useEffect(() => console.log("useEffect"))
  useLayoutEffect(() => console.log("useLayoutEffect"))
  return <div>ready</div>
}
/** 呼び出される順番 */
// useLayoutEffect
// useEffect
```

正直用途が。。描画結果が画面に反映されるまでにやること、、ウィンドウサイズを取得するとかですかね。
上手い使い方を知っている方がいらっしゃれば教えてくださいw

## 次回

Hookシリーズも大分きましたね。。さて次回は、、

**useReducer**

です。[Redux](https://redux.js.org/)って何だったんだって感じですよね。（あくまで筆者の見解です)

## 参考文献

[Reactハンズオンラーニング 第2版 ――Webアプリケーション開発のベストプラクティス](https://www.oreilly.co.jp/books/9784873119380/)