Skip to content

Instantly share code, notes, and snippets.

@dyerw
Last active August 18, 2021 21:06
Show Gist options
  • Save dyerw/ebb6fdf418ab2b5ce41577bf3c610b50 to your computer and use it in GitHub Desktop.
Save dyerw/ebb6fdf418ab2b5ce41577bf3c610b50 to your computer and use it in GitHub Desktop.
Anti-Reselect Manifesto
// From https://react-redux.js.org/api/hooks#using-memoizing-selectors
import { createSelector } from 'reselect'
// A Selector is just a fn from State to T
type Selector<T> = (state: State) => T;
const selectCompletedTodosCount = createSelector( // This utility does nothing you can't do easily without it
(state) => state.todos,
(_, completed) => completed, // Why would we do this? This is just a roundabout way of parameterizing a fn.
(todos, completed) =>
todos.filter((todo) => todo.completed === completed).length
)
// Instead simply:
// Create a basic selector
const selectTodos: Selector<Todo[]> = (state) => state.todos;
// Curry any parameters so that you end up with a selector (state arg in the tail of the arguments list)
// You can compose functions by calling them, you don't need reselect to do this.
const selectCompletedTodosCount2: (completed: boolean) => Selector<number> =
(completed) => (state) => selectTodos(state).filter((todo) => todo.completed === completed).length;
// But what about memoization? Reselect memoizes for us!
import { memoize } from "lodash"; // Or whatever other memo utility you want, they're out there
// Just memoize the expensive bit like you would anywhere else where performance was a concern, there's nothing
// special about selectors.
const getTodoCount = memoize(
(completed: boolean, todos: Todo[]) => todos.filter((todo) => todo.completed === completed).length
);
// And use it in your selector
const selectCompletedTodosCount3: (completed: boolean) => Selector<number> =
(completed) => (state) => getTodoCount(selectTodos(state), completed);
// Now we have a composable, memoized selector without reselect and we don't need to fight
// the library interface to pass a parameter
useSelector(selectCompletedTodosCount3(true));
// From the Reselect readme the advantages are:
// 1. Selectors can compute derived data, allowing Redux to store the minimal possible state.
// 2. Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
// 3. Selectors are composable. They can be used as input to other selectors.
// Rebuttal: Selectors are just functions, reselect didn't create anything new, just use functions.
// You can memoize functions.
// Watch:
// 1. Functions can compute derived data, allowing Redux to store the minimal possible state.
// 2. Memoized functions are efficient. A memoized function is not recomputed unless one of its arguments changes.
// 3. Functions are composable. They can be used as input to other functions.
// Fin
// Addendum:
// If you really like the createSelector composition interface it can be recreated with this one-liner utility fn
// and some variadic tuple types
type ExtractReturnTypes<T extends readonly ((i: any) => any)[]> = [
...{
[K in keyof T]: T[K] extends (i: any) => infer R ? R : never;
}
];
function composeSelectors<T, S extends Selector<any>[]>(
selectors: [...S],
resolver: (...args: ExtractReturnTypes<S>) => T,
): Selector<T> {
return (state: RootState) => resolver.apply(selectors.map((s) => s(state)));
}
const selectCompletedFilter = (state): boolean => state.completedFilter;
const selctedCompletedTodosCount4: Selector<number> = composeSelectors(
[selectCompletedTodosCount, selectCompletedFilter],
(todos, completed) => getTodoCount(completed, todos)
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment