はじめに
React ContextとReact Hooksでglobal stateを実現できるとか、いや、それはReduxと違ってパフォーマンスが出ないとか、そのような議論があります。これは、不要なrender(描画)を排除できるかどうかという論点で、規模が大きくなると影響が出る場合があるものです。本稿では、React ContextとReact Hooksで不要なrenderを排除する仕組みを備えるreact-trackedというライブラリを紹介します。
不要なrenderとは何か
例えば、global stateに二つの文字列を入れている場合、つまり、
const initialState = {
lastName: 'React',
firstName: 'Hooks',
};
このような形をしている場合があるとします。このstateがcontextを通してgloal stateとして提供するには次のようにProviderコンポーネントを作ります。
const Ctx = createContext();
const Provider = ({ children }) => {
const [state, setState] = useState(initialState);
return (
<Ctx.Provider value={[state, setState]}>
{children}
</Ctx.Provider>
);
};
一方、このglobal stateを利用して、firstNameを表示するコンポーネントは次のようになります。
const Component = () => {
const [{ firstName }] = useContext(Ctx);
return <div>{firstName}</div>;
};
これで機能的には問題ありません。しかし、不要なrenderが起こる場合があります。具体的には、firstNameが変更された場合だけでなく、lastNameが変更された場合にもこのコンポーネントはrenderされます。常に両方同時に変更される場合は問題ありませんが、lastNameだけが頻繁に変更される場合は、不要なrenderがパフォーマンス低下を引き起こす可能性があります。
この挙動はReact Contextの仕様であり、基本的な方針としては、更新タイミングが合わないデータを一つのcontextに入れるのではなくcontextを分割したり、コンポーネントを階層構造にしてReact.memoやuseMemoを使うことで改善したりすることが推奨されます。
React Trackedとは
一方で、どうしてもcontext分割しにくかったり、global stateとして管理する方が開発効率がいい場合もあります。そのようなケースに対応するのがReact Trackedというライブラリです。
ライブラリのドキュメントサイトはこちらです。
GitHubリポジトリはこちらです。
https://github.com/dai-shi/react-tracked
使い方
上記のfirstName/lastNameのstateの例をReact Trackedを使う場合、まずはじめにcontainerを作ります。
import { createContainer } from 'react-tracked';
const { Provider, useTracked } = createContainer(() => useState(initialState));
Providerは前回のものと同じように使い、useTrackedはuseContextの代わりに使います。つまり、先ほどのコンポーネントは次のようになります。
const Component = () => {
const [{ firstName }] = useTracked();
return <div>{firstName}</div>;
};
これだけの変更で、不要なrenderが排除できます。本稿では実装方法の詳細は省きますが、Proxyを使って実現しています。
ReduxのuseSelector
また、React TrackedのcontainerはuseSelectorも提供しており、Reduxのものとほぼ同様に使えます。これを使うと、次のように書けます。
const Component = () => {
const firstName = useSelector(state => state.firstName);
return <div>{firstName}</div>;
};
useSelectorはselector関数がシンプルな場合は良いですが、オブジェクトを生成したりするものの場合は、reselectなどを使ってmemoized selectorを作る必要があったりと、あまり初心者向きではないことが難点ではあります。
React Trackedの拡張性
containerを作成する際にuseStateの代わりにuseReducerを使うことができます。実は、stateを返すものなら何でも良いので、custom hooksを使うこともできます。
また、containerのuseTrackedやuseSelectorを拡張することもできます。
ドキュメントサイトにRecipesがあり、様々なパターンが載っています。
https://react-tracked.js.org/docs/recipes
Concurrent Mode対応
現在公開されているReactの実験的なバージョンでは、Concurrent Modeというのものが提供されています。詳しくは公式ドキュメントを参照してください。
Concurrent Modeを最大限に利用するには、React stateをベースにしていることが望ましいのですが、React TrackedはReact stateのラッパーなのでこれを満たしています。
一方、external storeを使っているライブラリ(ReduxやMobXなど)では、Concurrent Modeのある機能(state branchingと呼んでいます)が使えないという難点があります。
より詳しくはこちらのリポジトリをご参照ください。Concurrent Modeで課題になり得るポイントをテストするツールになっています。
おわりに
不要なrenderからConcurrent Modeまで話を詰め込みすぎた感があります。また、機会がありましたら個別の記事にしようかと思います。それまでは、ドキュメントサイトのQuick Startやブログ記事なども合わせてご参照ください。