Selectre

Time & space efficient state selectors for React, Redux, and more

npm install selectre

        import { createSelector } from "selectre";
        
        // use API similar to Reselect
        let selectCurrentUser = createSelector(
          // to make simple selectors without hustle
          (state) => state.users.currentUser,
        );
        
        let selectProjectById = createSelector(
          // seamlessly use parameters as selector input
          (state, projectId) => state.projects.byId[projectId],
          // pass more inputs to grab data from different state branches
          (state) => state.meta,
          // don't worry about returning complex data
          (projectInfo, meta) => ({ ...projectInfo, ...meta }),
        );
        
        function ProjectInfo({ projectId }) {
          // call the select function to get a properly cached selector
          let currentUser = useSelector(selectCurrentUser());
          // which also allows you to pass parameters, if there any
          let projectInfo = useSelector(selectProjectById(projectId));
        
          return <JSX />;
        }
      

About

Getting Started

🚧 work in progress 🚧

Configuration

🚧 work in progress 🚧

Types

Selectre includes necessary type definitions to enhance developers experience when using TypeScript. Writing selectors in TypeScript, there are a couple of things you may encounter and this guide should help addressing them.

State parameter should always be typed. Selectors do not know where the state is coming from and who is calling them. To improve developers experience it is better to explicitly define state's type:

        let selectData = createSelector(
          (state: MyStateType) => state.a.b.c.data,
        );
      

Parametric selectors should enumerate all parameters in the first input. The way how type definitions are written, TypeScript expects all input functions to have the same signatures (because all of them receive the same parameters during computation). It is likely that some inputs may not use all parameters, so it is fine to skip some of them. But TypeScript relies on the very first input of a selector to determine what parameters to look for in the following inputs and in the selector signature:

        let selectDataByParams = createSelector(
          (state: MyState, id: string, offset: number) => state.data.slice(offset, 10),
          (state: MyState, id: string) => state.some.other.stuff[id],
          (data, stuff) => [data, stuff],
        );
      

Comparison to Reselect

  1. Selectre uses shallow equality by default, Reselect uses strict equality by default
  2. Selectre uses LRU cache by default, no need to instantiate and memoize selectors in components
  3. Selectre's LRU cache is implemented as Doubly-Linked List + Hash Map for perfomance and better memory allocation. Reselect's LRU is O(n) for get and O(n) for set operations. Frequent use of array's splice() and unshift() in Reselect is an unnecessary performance burden
  4. Parametric selectors in Reselect require to use arrow function with useSelector() which means additional selector reads during forced re-render
  5. Result of createSelector() in Selectre is not selector itself but an accessor to the selector and its cached result

Getting into more details, let's consider the example that was described before:

        import { createSelector } from "selectre";
        import { useSelector } from "react-redux";
        
        let selectNumberFilteredTodos = createSelector(
          (state) => state.todos,
          (_, completed) => completed,
          (todos, completed) => todos.filter((todo) => todo.completed === completed).length,
        );
        
        function TodoCounter({ completed }) {
          let numberFilteredTodos = useSelector(selectNumberFilteredTodos(completed));
          return <span>{numberFilteredTodos}</span>;
        }
      

A simple case of a selector with parameters, being used with Redux's useSelector(). Values in the selector are compared using shallow equality by default, nothing needs to be configured manually. If you want to have the same behavior implemented with Reselect, here is what needs to be done:

        import { useMemo } from "react";
        import { createSelector } from "reselect";
        // 1. Need to explicitly set shallowEqual as a second param of useSelector
        import { shallowEqual, useSelector } from "react-redux";
        
        // 2. need to make a factory function, because selector instances can't be shared by default
        let makeSelectNumberFilteredTodos = () =>
          createSelector(
            (state) => state.todos,
            (_, completed) => completed,
            (todos, completed) => todos.filter((todo) => todo.completed === completed).length,
          );
        
        function TodoCounter({ completed }) {
          // 3. additional friction in order to start using a selector
          let selectNumberFilteredTodos = useMemo(makeSelectNumberFilteredTodos, []);
          // 4. still need to use arrow function in useSelector() which means additional read operation
          let numberFilteredTodos = useSelector(
            (state) => selectNumberFilteredTodos(state, completed),
            shallowEqual,
          );
          return <span>{numberFilteredTodos}</span>;
        }
      

This simple case requires developer to know plenty of details, just to make sure that a selector that returns an object does not affect performance. It is something that quite easy to overlook, e.g. when you only update selector's output from primitive value to object.

Credits

The intent and main API is similar and based on Reselect.

LRU cache implementation is based on Guillaume Plique's article about LRU cache and using typed arrays to implement Doubly-Linked Lists (GitHub: @Yomguithereal, Twitter: @Yomguithereal).

Cache key's hash function implementation is based on Immutable.js hashCode().