Reselectの使い方

ReduxなどのステートコンテナはReactクライアントのデータフロー整備に不可欠です。しかし、素朴に実装するとレンダリング回数が多くなりがちです。

Reselectはmemoize手法により、再計算を抑制するライブラリです。
典型的な利用シーンは、API取得したデータが大きなリストになっていて、各コンポーネントではそのうちの一部を抽出する場合です。 Reactなどコンポーネントが提供するmemoizeはレンダリングの抑制には効きますが、抽出する関数じたいは実行する点が異なります。

インストール

Reselectは関数一般にmemoizeを提供するライブラリであり、npm install reselectで単体インストール可能です。
Reduxを利用している場合にはRedux Toolkitにバンドルされているため、toolkitからインポートできます。

import { createSelector } from '@reduxjs/toolkit'

React Hooks主体で開発するケースでは、基本的にReduxのuseSelector()と合わせて利用するケースが多いでしょう。

利用方法

createSelector()のリファレンスが最も重要です。
Reselectを利用するうえでは、周辺のドキュメントも参考に挙動を理解した方が良いのですが、createSelector()じたいの使い方も独特です。

  • 引数には複数の関数をとり、最後の関数の返す値がコンポーネントに利用される。他の関数は、最後の関数への引数として接続される
  • 関数(ただし最後以外)の引数は、(state, props)を受けとり他の引数は指定できない。なお、propsは使用しない場合、省略可能
  • createSelector()はコンポーネント関数外で使用する必要がある

といった構造になっており、読みとりにくいポイントです。

以下のような実装例になります。

// Selector定義
export const taskSelector = createSelector(
    [
        (state, _) => state.tasks,
        (_, id) => id,
    ],
    (tasks, id) => tasks[id]
);

// 呼び出し方
const task = taskSelector(state, props.id);

この関数は2層構造になっています。第1引数の配列が1層目でその結果が第2引数の関数に渡ります。

この関数の例では呼び出し時の引数が2つあるため、配列内の関数では各引数を加工しています。
配列内の各関数には同じ引数が渡されているという点も分かりづらいポイントです。

2層目の関数は呼び出し時の引数を直接とらないため、一見無意味な(_, id) => idは必要です。

必要性の補足

Reduxは単一オブジェクトにサブツリーを複数持つ構造になります。
サブツリーのどこかが変更されると、全体的にステート再計算をリクエストします。

ストアのオブジェクトが大きくなると、コンポーネントが参照していないサブツリーの変更の影響をかなり受けることとなり、じつは想像しているよりもレンダリング回数は多くなっています。場合によっては無限ループも起きます。

Reduxの標準機能にもshallowEqualといったレンダリング抑制機能がありますが、ネストしたオブジェクトには機能しないため、事実上かなりのケースでReselectなどのmemoizeを実装した方が良いでしょう。

なおRedux/ReselectはReactに依存しておらず、 Litとの組み合わせでも期待どおり動作します。

Reactは描画レイヤの過レンダリング傾向が強い問題がありますが、Litなどはdiff検出の工夫により描画のmemoizeを原則不要としています。

Reselectには引数のdiffロジックをカスタマイズするオプションもあり、Reselectを適切に実装するだけで性能チューニングをほぼカバーできます。

中馬崇尋
Chuma Takahiro