React初心者が気をつけたいコンポーネント実装のポイント、パフォーマンスチューニング

# 自己紹介
こんにちは。Standard Cognition（ https://standard.ai )というSFにある会社でSoftware Engineerをやっている[Kenta](https://twitter.com/hellokenta_ja)です。

フロントエンド関連の事や、アメリカでのエンジニア経験などつぶやいていくので、ぜひ**Twitterのフォローもお願いします！** [@hellokenta_ja](https://twitter.com/hellokenta_ja)

# 1. 背景
[Hooks](https://ja.reactjs.org/docs/hooks-intro.html)などの導入もあり、Reactのコンポーネントが非常に簡単に書けるようになりました。簡単に書ける分、知らず知らずのうちにアンチパターン的な実装をしてしまっている事がよくあると思っています。僕もReactを始めた当初たくさん地雷を踏んでいました、、、😭

そのような実装をしてしまった時に起こる問題としては

- 無駄なレンダリングが起きてパフォーマンスが落ちる
- 予期しないバグを生む
- アプリは通常に動いているように見えるので、上記は非常に気付きづらい

などがあります。
アプリケーションが小さいうちはパフォーマンスなどそこまで問題にならないのですが、コツコツと積み上がったパフォーマンスの負債は、アプリの規模が大きくなった時に現れてきます。
そんな僕も、初期にたくさん引っかかった実装のポイントをいくつか紹介していきます！:tada:

# 2. まず、Reactコンポーネントはどんな時に再レンダーされるのか理解しよう
パフォーマンスを改善するには、まず**Reactコンポーネントがどのタイミングで再レンダーされるのか**を理解する必要があります。Reactコンポーネントが再レンダーされるのは下記のタイミングです。

- 新しい`props`を受け取った時
- `state`が変更された時
- リッスンしている`Context`の値が変更された時
- 親のコンポーネントが上記の理由で再レンダーされた時

**参考**
https://stackoverflow.com/questions/55106951/react-with-hooks-when-do-re-renders-happen

かつ、かつ**再レンダーとアンマウントは別**です。
再レンダーとは、コンポーネントに対して`render`関数を呼び出す事であり、`state`は保持されます。アンマウントされると、そのコンポーネントのインスタンスは破棄され、`state`も破棄されます。
**参考**
https://ja.reactjs.org/docs/reconciliation.html

# 3. Stateはなるべく子コンポーネントで持とう
上記で書いたように、親コンポーネントが再レンダーされると、子コンポーネントまで再レンダーされてしまいます。なので、なるべく親コンポーネントで`state`を管理するのはやめて、**最低限そのデータが必要な子コンポーネントでそのデータを読むようにしましょう。**
例えば

- `/products/:product_id`のページ全体に対応する親コンポーネントで`product_id`を取得する
- その`Product`のデータを取ってくるAPIコールは親コンポーネントで行う
- 子コンポーネントには`product_id`だけPropsとして渡し、その`product_id`に対応するデータを`Redux State`などから取得する

という感じです。子コンポーネントにpropsとしてデータを渡さない事は、コンポーネントとして汎用性が低くなり、テストがしづらくなるなどトレードオフもありますので、そこはバランスを見て実装しましょう。

どうしても親で`state`を持たなければいけなく、かつ子コンポーネントの再レンダーを防ぎたい場合は、`memo`や`useMemo`を利用しましょう。
**参考**
https://medium.com/@guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928

# 4. Renderメソッド内でコンポーネントやコンポーネントのスコープとは関係のない関数を作ったりしない
### Before
```react
const Sample = () => {
  const Inner = ({ value }) => {
    return <div>Value is {value}</div>
  }
  const add = (a, b) => a + b;

  return (
    <>
      <Inner value={add(1,2)} />
    </>
  )
}
```

上記の実装だと、`Sample`がrenderされる度に`Inner`のインスタンスは新しく作られてマウントされてしまします。同様に`add`メソッドも無駄に作り直されてしまいます。

### After
```react
const Inner = ({ value }) => {
  return <div>Value is {value}</div>
}

const add = (a, b) => a + b;

const Sample = () => {
  const value = add(1,2)
  return (
    <>
      <Inner value={value} />
    </>
  )
}
```

# 5. useEffectやuseStateは条件分岐の中で呼び出してはいけない
```react
const Sample = (props) => {
  if (!props) {
    useEffect(() => {
      //なにかの処理
    });
  }
  if (!props) return <div>早期リターン</div>

  const [name, setName] = useState('Kenta')
  return (
    <>{name}</>
  );
};
```
上記のように、`useEffect`や`useState`が条件によって**呼ばれたり呼ばれなかったりする実装はやめましょう。**理由は、React内部の実装が、それらが呼ばれる順番に依存した実装をしているからです。
**参考**
https://ja.reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

# 6. APIコールなどは、useEffect内で呼び出し、ちゃんとDependenciesを設定する
APIコールなどの関数呼び出しをコンポーネントの関数内で直接呼ぶのは、**render毎に呼ばれてしまう**ので避けましょう。
また、`useEffect`内で呼び出したとしても、`Dependencies`を設定しないと、render毎にその処理が呼ばれてしまいます。

なので**「何の値が変更されたらその処理を走らせたいのか」**を考慮して、適切な`Dependencies`を設定しましょう。APIコールなど、マウント時に一度しか呼ばなくて良い処理は`[]`を`Dependencies`に設定しましょう。

```react
const Sample = (props) => {
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(getProducts()) // APIコール処理
  }, []) // <- これがDependencies。マウント時に一度だけ呼ばれる

  return <></>
};
```

# 7. useEffectのDependenciesにFunctionやArrayを渡す際の注意点
```react
const Sample = (props) => {
  const fetchData = async () => {
    // fetch data code
  }

  useEffect(() => {
    fetchData()
  }, [fetchData])

  return <></>
};
```

上記の例はうまく動きません。
なぜなら、render毎に`fetchData`は新しく作られるので、`useEffect`内の処理もrender毎に呼ばれます。結果的に、render毎にAPIコールを呼んでしまうことになります。

解決策としては、

`fetchData`関数を`useEffect`内に入れる。

```react
const Sample = (props) => {
  useEffect(() => {
    const fetchData = () => {
      // fetch data code
    }
    fetchData()
  }, [])

  return <></>
};
```

`fetchData`がコンポーネントのスコープと関係ないなら外に出してしまう。

```react
const fetchData = () => {
  // fetch data code
}
const Sample = (props) => {
  useEffect(() => {
    fetchData()
  }, [])

  return <></>
};
```

また、`Array`や`Object`のような**参照型**のものを`Dependencies`として持たせる場合も気をつけましょう。実際の中身は変わっていなくても、参照が変わっていたら`useEffect`の中の処理は呼ばれてしまいます。ですので、`join`や`JSON.stringfy`で文字列にするなどの工夫をする必要があります。

# 8. useSelectorで値をひとまとめにしない
```react
const { name, age } = useSelector(state => state.user)
```
`react-redux`を使う場合、`useSelector`を利用して`state`から値を取ってくるのですが、`useSelector`は`===`によって同値性を判断しています。ですのでこのように`destructive`に値を取得してしまうと、`user`のReferenceが変更される度に、もし`name`や`age`の値が変更されていなくても、値が変わったとみなされてrenderが走ってしまいます。

なので、下記のようにそれぞれで`useSelector`を呼び出す必要があります。

```react
const name = useSelector(state => state.user.name)
const age = useSelector(state => state.user.age)
```

**参考**
https://react-redux.js.org/api/hooks#useselector

# 9. List表示の`key`に`index`を使用しない

`index`を利用してしまっている例

```react
{products.map((product, index) =>
  <Product
    {...product}
    key={index}
  />
)}
```

Reactは、List表示のレンダーを検討する際、子要素を順番に見ていって、差分を見つけたところで更新を発生させます。順番に依存しているので、ほとんどの要素は変更するべきではないのに、順番が変わっただけで全ての子要素を再レンダーしてしまう事もありえます。

ですので、`key`にはその要素に対して一意なもの（`uuid`など）を渡しましょう。

# 10. ReduxのStateをノーマライズしましょう
こちらはあくまで実装のコストなどのバランスも検討して必要に応じて、という事になりますが、`state`自体の最適化をする事によって、不必要なレンダーを防ぐことも出来ます。

### Before

```javascript
const products = [
  {
    id: 'product1',
    producer: { name: 'Kenta inc', id: 'kenta' },
    skus: [
      {
        id: 'sku1',
        name: 'White T shirt'
      },
      {
        id: 'sku2',
        name: 'Black T shirt'
      }
    ]
  },
  {
    id: 'product2',
    producer: { name: 'Hara inc', id: 'hara' },
    skus: [
      {
        id: 'sku3',
        name: 'White cap'
      },
      {
        id: 'sku4',
        name: 'Black cap'
      }
    ]
  },
]
```

### After

```javascript
const reducer = {
  products: {
    byId: {
      "product1": {
        id: "product1",
        producer: "kenta",
        skus: ["sku1", "sku2"]
      },
      "product2": {
        id: "product2",
        producer: "hara",
        skus: ["sku3", "sku4"]
      }
    },
    allIds: ["product1", "product2"]
  },
  producers: {
    byId: {
      "kenta": {
        id: "kenta",
        name: "Kenta inc"
      },
      "hara": {
        id: "hara",
        name: "Hara inc",
      }
    },
    allIds: ["kenta", "hara"]
  },
  skus: {
    buId: {
      "sku1": {
        id: "sku1",
        name: "White T shirt"
      },
      "sku2": {
        id: "sku2",
        name: "Black T shirt"
      },
      "sku3": {
        id: "sku3",
        name: "White cap"
      }
    },
    allIds: ["sku1", "sku2", "sku3"]
  }
}
```

このようにノーマライズすることによって、

- データのネストが少なくなったのでデータの取得や更新が簡単になる
- データが分離されたので、データに更新があった時にUIの変更が少なくすむ
    - 例えば`allIds`の配列の順番が変更されただけでは、データの詳細にあたる`byId`の変更は無いので、それに対応するUIの変更はなくなります。

**参考**
https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape

# 11. パフォーマンス・チューニングの仕方
僕は主にReactのDeveloper Toolを利用して無駄なレンダーなどを特定しています。使い方などは割愛するので、下記を参考にお願いします。

- https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en
- https://ja.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html
- https://ja.reactjs.org/docs/optimizing-performance.html

# 12. まとめ
以上、Reactで初心者が気をつけてほしい実装ポイントでした。
僕は普段FPSを気にしたReactアプリケーションの実装をしているので大分ここら辺のパフォーマンスに注意して実装しているのですが、上記の事をケアすることによって不必要なAPIコールなどのバグも防げると思います。

最後まで読んでいただきありがとうございました。
Twitterでも、Reactやフロントエンドに関するツイートや、アメリカでのエンジニア経験に関してツイートしているので、よかったらフォローお願いします:tada:
Twitter: [@hellokenta_ja](https://twitter.com/hellokenta_ja)