Last active
August 18, 2021 21:06
-
-
Save dyerw/ebb6fdf418ab2b5ce41577bf3c610b50 to your computer and use it in GitHub Desktop.
Anti-Reselect Manifesto
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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