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を用いたサンプルアプリケーションを作成しました。
コードとしてはこのようになります。
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
の値を更新/追加するとどうなるでしょうか?
user
を更新した際はUserInfoコンポーネントのみ再レンダリングされ、posts
を更新した時はPostListコンポーネントのみ再レンダリングされます。
Contextの再レンダリング
今度はContextについてみていきます、以下のようなContextを用いたサンプルアプリケーションを作成しました(Reduxのものと見た目は一緒です)。
コードとしてはこのようになります。
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
の値を更新/追加してみましょう。
Reduxとは違い、user
、posts
のうち片方を更新すると、UserInfoコンポーネントとPostListコンポーネントの両方が再レンダリングされていることがわかります。
user
を更新したのにPostListコンポーネントが再レンダリングされてしまう、もしくはその逆のパターンの抑制策としてはReact.memo
を用いmemo化を行う方法があります。本記事では詳しく説明しませんが、もし興味がある方は以下を確認してみてください。
4. 終わりに
以上のように今回はReduxとContextの再レンダリングの違いについて、文字だけでなく実際にサンプルアプリケーションを用いて確認していきました。
本記事のサンプルコードは以下になります。コード全体がみたいという方はこちらを参照してください。
https://github.com/pipopotamasu/redux-context-comparison
明日は弊社のEM兼バックエンドエンジニアであるhamがお送りします。