ReduxとContextの再レンダリングの違いをサンプルアプリケーションと共に確認する

VISITS Technology株式会社( https://visits.world/ )の2020年アドベントカレンダー１発目を努めさせていただきます[pipopotamsau](https://twitter.com/pipopotamasu3)です。

https://qiita.com/advent-calendar/2020/visits

業務ではフロントエンドエンジニアとしてReactを用いたアプリケーションを開発していますので、React関連について書きたいと思います。

## 1. ReduxとContextについて
Reactアプリケーションでデータ、特にグローバルなデータをストアしたい場合Reduxを用いるか、はたまたContextを使うかという議論があるかと思います(最近はHTTP Requestをキャッシュするライブラリなどが出てきて色々選択肢が増えましたが[^1]、本記事の本筋とは関係ないため言及しません)。

どちらを使うべきであるかは開発の前提条件にもよりますので一概には言えませんが、論点の中には「パフォーマンス」に関するものがあります。

## 2. パフォーマンスについての議論
おそらく「Redux vs Context」でググると、「頻繁にグローバルストアを更新するんならReduxの方がパフォーマンスいいよ」的なものが出てくると思います。これは何故でしょうか？

これは両者の状態が更新されたときに発生する再レンダリングのスコープが違うからです。

**Redux: 変更された値をサブスクライブしているコンポーネントのみ再レンダリングが走る**
**Context: 変更されたContextのProviderの子コンポーネント以下全てに再レンダリングが走る**

このような違いがあるため「頻繁にグローバルストアを更新するんならReduxの方がパフォーマンスいいよ」という話が出てきます。

## 3. ReduxとContextの再レンダリングの違いをサンプルアプリケーションで確認してみる
さて、ここからが本番です。
上記でどのような再レンダリングの違いがあるかを言葉で書きましたが、今度は実際にサンプルアプリケーションで確認していきましょう。

※ 本記事のサンプルコードのレポジトリは[こちら](https://github.com/pipopotamasu/redux-context-comparison
)です

前提として、ReduxとContextそれぞれのサンプルアプリケーションで以下のようなグローバルステートを保持するとします。

```
{
  user: { id: 1, name: 'test name' },
  posts: [{ id: 1, title: 'test post' }]
}
```

上記のステートを更新したときに、どのような再レンダリングの違いがあるかをみていきます。

### Reduxの再レンダリング
前述したグローバルステートを持った、以下のようなReduxを用いたサンプルアプリケーションを作成しました。

![redux-sample-application.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199424/dec115b5-8180-fde6-d183-1566a33a6c48.png)

コードとしてはこのようになります。

```react
import { Provider, useSelector, useDispatch } from 'react-redux';
import { store } from '../reduxStore';
import classes from '../styles/page.module.css';

function UserInfo () {
  console.log('UserInfo is updated!');
  const user = useSelector(state => state.user);
  const dispatch = useDispatch();

  return (
    <div className={classes.userContainer}>
      <p>{user.name}</p>
      <button
        onClick={() => {
          dispatch({ type: 'UPDATE_NAME', payload: 'updated name' });
        }}
      >
        update user
      </button>
    </div>
  )
}

function PostList () {
  console.log('PostList is updated!');
  const posts = useSelector(state => state.posts);
  const dispatch = useDispatch();

  return (
    <div className={classes.postsContainer}>
      <ul>
        {
          posts.map(post => <li key={post.id}>{post.title}</li>)
        }
      </ul>
      <button
        onClick={() =>  {
          dispatch(
            { type: 'ADD_POST', payload: { id: posts.length + 1, title: 'added post' }}
          )
        }}
      >
        add post
      </button>
    </div>
  )
}

export default function ReduxExample () {
  return (
    <Provider store={store}>
      <h1>Redux</h1>
      <UserInfo />
      <PostList />
    </Provider>
  )
}

```

親コンポーネントとしてReduxExampleコンポーネントがあり、ReduxのstoreをProvideしています。
また、子コンポーネントとして`user`を参照しているUserInfoコンポーネントと`posts`を参照しているPostListコンポーネントがあります。

このような構成で`user`と`posts`の値を更新/追加するとどうなるでしょうか？

![redux-demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199424/0698290e-36a5-e183-1a19-b973e1388d8f.gif)

`user`を更新した際はUserInfoコンポーネントのみ再レンダリングされ、`posts`を更新した時はPostListコンポーネントのみ再レンダリングされます。

### Contextの再レンダリング
今度はContextについてみていきます、以下のようなContextを用いたサンプルアプリケーションを作成しました(Reduxのものと見た目は一緒です)。

![context-sample-application.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199424/1f2a330c-9c39-8a5c-a6e6-5e12567676b4.png)

コードとしてはこのようになります。

```react
import { useContext, useReducer } from 'react';
import { initialState, reducer, Context } from '../context';
import classes from '../styles/page.module.css';

function UserInfo () {
  console.log('UserInfo is updated!');
  const { user, dispatch } = useContext(Context);

  return (
    <div className={classes.userContainer}>
      <p>{user.name}</p>
      <button
        onClick={() => dispatch({ type: 'UPDATE_NAME', payload: 'updated name' })}
      >
        update user
      </button>
    </div>
  )
}

function PostList () {
  console.log('PostList is updated!');
  const { posts, dispatch } = useContext(Context);

  return (
    <div className={classes.postsContainer}>
      <ul>
        {
          posts.map(post => <li key={post.id}>{post.title}</li>)
        }
      </ul>
      <button
        onClick={() =>  {
          dispatch(
            { type: 'ADD_POST', payload: { id: posts.length + 1, title: 'added post' }}
          )
        }}
      >
        add post
      </button>
    </div>
  )
}

export default function ContextExample () {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <Context.Provider value={{ user: state.user, posts: state.posts, dispatch }}>
      <h1>Context</h1>
      <UserInfo />
      <PostList />
    </Context.Provider>
  )
}

```
親コンポーネントとしてContextExampleコンポーネントがあり、ContextのステートをProvideしています。
また、子コンポーネントとして`user`を参照しているUserInfoコンポーネントと`posts`を参照しているPostListコンポーネントがあります。

Reduxのサンプルアプリケーションと同様に、こちらも`user`と`posts`の値を更新/追加してみましょう。

![context-demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199424/1531894c-b7cf-33c6-a879-c40157ccddbe.gif)

Reduxとは違い、`user`、`posts`のうち片方を更新すると、UserInfoコンポーネントとPostListコンポーネントの両方が再レンダリングされていることがわかります。

`user`を更新したのにPostListコンポーネントが再レンダリングされてしまう、もしくはその逆のパターンの抑制策としては`React.memo`を用いmemo化を行う方法があります。本記事では詳しく説明しませんが、もし興味がある方は以下を確認してみてください。

https://ja.reactjs.org/docs/react-api.html#reactmemo

## 4. 終わりに
以上のように今回はReduxとContextの再レンダリングの違いについて、文字だけでなく実際にサンプルアプリケーションを用いて確認していきました。

本記事のサンプルコードは以下になります。コード全体がみたいという方はこちらを参照してください。
https://github.com/pipopotamasu/redux-context-comparison

明日は弊社のEM兼バックエンドエンジニアである[ham](https://qiita.com/ham0215)がお送りします。

[^1]: react-query等。 https://github.com/tannerlinsley/react-query/discussions/329