reselect の仕組みと使い方

背景

Reduxのドキュメントを見ていると「reselect」というライブラリについて書かれており、うまく利用すればパフォーマンスチューニング(システムのパフォーマンスを向上させること)できることを知りました。

今回 ドキュメントを読み進めていく中で reselect についていろいろなことを知り、自分なりに使用方法やreselectの内部で起きているメモ化についてわかりやすくまとめてみました。

これからreselectを使用する人の参考になれば幸いです。

reselect とは?

reselect とはメモ化(変数・関数・コンポーネントのキャッシュを再利用しコンポーネントの不要な再レンダリングを抑えること)された Selector 関数(Store の State に基づいたデータを返す関数。ReactでいうとuseSelectorなど)を作成するためのReduxライブラリです。

Selector関数をメモ化するので、不要なSelector関数の再計算を防ぐことができます。

また、reselectは、入力値に基づいてメモ化されたSelector 関数(outputSelector)を生成するcreateSelector APIを提供します。

使用方法は、メモ化されたSelector関数を生成する createSelector を component に import することで使用することができます。

createSelectorは、入力セレクタ(inputSelector)と結果関数(resultFunc)を受け取り、抽出された値が変更されたときにのみ出力を再計算します。

基本的な使い方

const outputSelector = createSelector(
  [inputSelector1, inputSelector2, inputSelector3], // synonymous with `dependencies`.
  resultFunc // Result function
)

使用例は下記の通りです。(ほぼドキュメントを抜粋したもの)

import React from 'react';
import { createSelector } from 'reselect';

const ReselectExample = () => {
  const state = {
    todos: [
      { id: 0, completed: false },
      { id: 1, completed: true }
    ],
    alerts: [
      { id: 0, read: false },
      { id: 1, read: true }
    ]
  };

  const selectCompletedTodos = (state) => {
    console.log('selector ran');
    return state.todos.filter(todo => todo.completed === true);
  };

  selectCompletedTodos(state); // selector ran
  selectCompletedTodos(state); // selector ran
  selectCompletedTodos(state); // selector ran

  const memoizedSelectCompletedTodos = createSelector(
    [(state) => state.todos],
    (todos) => {
      console.log('memoized selector ran');
      return todos.filter(todo => todo.completed === true);
    }
  );

  memoizedSelectCompletedTodos(state); // memoized selector ran
  memoizedSelectCompletedTodos(state);
  memoizedSelectCompletedTodos(state);

  console.log(selectCompletedTodos(state) === selectCompletedTodos(state)); //=> false

  console.log(
    memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state)
  );

  return (
    <>
    </>
  );
};

export default ReselectExample;

上記のコードをコピペし、google dev tools の console で確認していただくと、memoizedSelectCompletedTodosは 2度目、3度目は実行されず、前回と同じ戻り値を返しています。

このように、不要な再計算をスキップ(メモ化)するだけでなく、memoizedSelectCompletedTodos は、再計算がなければ前回キャッシュされた結果の値を返します。

また、reselectのメモ化は通常のメモ化とは異なります(reselectのメモ化を Cascading Memoization と言います)。

通常のメモ化だと、下記のように与えられた引数が前回と同じ値かどうか判断し、同じなら前回と同じ結果を返し、そうでないなら再び関数を実行します。

ただし、reselectのメモ化 Cascading Memoization だと、①与えられた引数が前回と同じ値かどうか比較する ②入力セレクタ(input selectors)を実行し、現在の結果を以前の結果と比較する

ように 段階的に各値を比較し、キャッシュされた結果を返すだけか、結果関数(result function)を実行するかの判断を行います。

このようにしてreselectはメモ化を行い、不要な再計算を行わないようにしています。

まとめ

今回、Reduxライブラリの reselect の使い方やreselect内で行われているメモ化について解説いたしました。

個人的には、Cascading Memoization の仕組みが自分の頭の中で整理することができたので、良かったです。

また、実務ではまだ使用したことがないので、機会があったら使用してみたいと思います。

現在デザイン・システム室では、新しいメンバーを募集しています。

少しでも興味を持たれた方、ぜひご応募ください。😄

参考

https://github.com/reduxjs/reselect?tab=readme-ov-file

https://reselect.js.org/introduction/getting-started

https://reselect.js.org/introduction/how-does-reselect-work