14
4

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 5 years have passed since last update.

React HooksAdvent Calendar 2019

Day 5

react-trackedの紹介

Last updated at Posted at 2019-12-04

はじめに

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

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

https://react-tracked.js.org

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

14
4
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
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?