react-trackedの紹介

## はじめに

React ContextとReact Hooksでglobal stateを実現できるとか、いや、それはReduxと違ってパフォーマンスが出ないとか、そのような議論があります。これは、不要なrender(描画)を排除できるかどうかという論点で、規模が大きくなると影響が出る場合があるものです。本稿では、React ContextとReact Hooksで不要なrenderを排除する仕組みを備えるreact-trackedというライブラリを紹介します。

## 不要なrenderとは何か

例えば、global stateに二つの文字列を入れている場合、つまり、

```javascript
const initialState = {
  lastName: 'React',
  firstName: 'Hooks',
};
```

このような形をしている場合があるとします。このstateがcontextを通してgloal stateとして提供するには次のようにProviderコンポーネントを作ります。

```jsx
const Ctx = createContext();

const Provider = ({ children }) => {
  const [state, setState] = useState(initialState);
  return (
    <Ctx.Provider value={[state, setState]}>
      {children}
    </Ctx.Provider>
  );
};
```

一方、このglobal stateを利用して、firstNameを表示するコンポーネントは次のようになります。

```jsx
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というライブラリです。

ライブラリのドキュメントサイトはこちらです。

<https://react-tracked.js.org>

GitHubリポジトリはこちらです。

<https://github.com/dai-shi/react-tracked>

## 使い方

上記のfirstName/lastNameのstateの例をReact Trackedを使う場合、まずはじめにcontainerを作ります。

```javascript
import { createContainer } from 'react-tracked';

const { Provider, useTracked } = createContainer(() => useState(initialState));
```

Providerは前回のものと同じように使い、useTrackedはuseContextの代わりに使います。つまり、先ほどのコンポーネントは次のようになります。

```jsx
const Component = () => {
  const [{ firstName }] = useTracked();
  return <div>{firstName}</div>;
};
```

これだけの変更で、不要なrenderが排除できます。本稿では実装方法の詳細は省きますが、Proxyを使って実現しています。

## ReduxのuseSelector

また、React TrackedのcontainerはuseSelectorも提供しており、Reduxのものとほぼ同様に使えます。これを使うと、次のように書けます。

```jsx
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というのものが提供されています。詳しくは[公式ドキュメント](https://reactjs.org/docs/concurrent-mode-intro.html)を参照してください。

Concurrent Modeを最大限に利用するには、React stateをベースにしていることが望ましいのですが、React TrackedはReact stateのラッパーなのでこれを満たしています。

一方、external storeを使っているライブラリ（ReduxやMobXなど）では、Concurrent Modeのある機能（state branchingと呼んでいます）が使えないという難点があります。

より詳しくは[こちらのリポジトリ](https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode)をご参照ください。Concurrent Modeで課題になり得るポイントをテストするツールになっています。

## おわりに

不要なrenderからConcurrent Modeまで話を詰め込みすぎた感があります。また、機会がありましたら個別の記事にしようかと思います。それまでは、ドキュメントサイトの[Quick Start](https://react-tracked.js.org/docs/quick-start)や[ブログ記事](https://blog.axlight.com/posts/4-options-to-prevent-extra-rerenders-with-react-context/)なども合わせてご参照ください。