8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

VISITSAdvent Calendar 2020

Day 1

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

Last updated at Posted at 2020-11-30

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

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

1. ReduxとContextについて

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

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

2. パフォーマンスについての議論

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

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

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

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

3. ReduxとContextの再レンダリングの違いをサンプルアプリケーションで確認してみる

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

※ 本記事のサンプルコードのレポジトリはこちらです

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

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

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

Reduxの再レンダリング

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

redux-sample-application.png

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

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コンポーネントがあります。

このような構成でuserpostsの値を更新/追加するとどうなるでしょうか?

redux-demo.gif

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

Contextの再レンダリング

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

context-sample-application.png

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

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のサンプルアプリケーションと同様に、こちらもuserpostsの値を更新/追加してみましょう。

context-demo.gif

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

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

4. 終わりに

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

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

明日は弊社のEM兼バックエンドエンジニアであるhamがお送りします。

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

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?