Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

react-trackedの紹介

はじめに

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リポジトリはこちらです。

使い方

上記の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があり、様々なパターンが載っています。

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ブログ記事なども合わせてご参照ください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
4
Help us understand the problem. What are the problem?