UNPKG

27.3 kBMarkdownView Raw
1# Reselect
2
3A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well.
4
5- Selectors can compute derived data, allowing Redux to store the minimal possible state.
6- Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
7- Selectors are composable. They can be used as input to other selectors.
8
9The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical Reselect usage patterns, and using selectors with React-Redux.
10
11[![GitHub Workflow Status][build-badge]][build]
12[![npm package][npm-badge]][npm]
13[![Coveralls][coveralls-badge]][coveralls]
14
15## Installation
16
17### Redux Toolkit
18
19While Reselect is not exclusive to Redux, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed.
20
21```js
22import { createSelector } from '@reduxjs/toolkit'
23```
24
25### Standalone
26
27For standalone usage, install the `reselect` package:
28
29```bash
30npm install reselect
31
32yarn add reselect
33```
34
35## Basic Usage
36
37Reselect exports a `createSelector` API, which generates memoized selector functions. `createSelector` accepts one or more "input" selectors, which extract values from arguments, and an "output" selector that receives the extracted values and should return a derived value. If the generated selector is called multiple times, the output will only be recalculated when the extracted values have changed.
38
39You can play around with the following **example** in [this CodeSandbox](https://codesandbox.io/s/objective-waterfall-1z5y8?file=/src/index.js):
40
41```js
42import { createSelector } from 'reselect'
43
44const selectShopItems = state => state.shop.items
45const selectTaxPercent = state => state.shop.taxPercent
46
47const selectSubtotal = createSelector(selectShopItems, items =>
48 items.reduce((subtotal, item) => subtotal + item.value, 0)
49)
50
51const selectTax = createSelector(
52 selectSubtotal,
53 selectTaxPercent,
54 (subtotal, taxPercent) => subtotal * (taxPercent / 100)
55)
56
57const selectTotal = createSelector(
58 selectSubtotal,
59 selectTax,
60 (subtotal, tax) => ({ total: subtotal + tax })
61)
62
63const exampleState = {
64 shop: {
65 taxPercent: 8,
66 items: [
67 { name: 'apple', value: 1.2 },
68 { name: 'orange', value: 0.95 }
69 ]
70 }
71}
72
73console.log(selectSubtotal(exampleState)) // 2.15
74console.log(selectTax(exampleState)) // 0.172
75console.log(selectTotal(exampleState)) // { total: 2.322 }
76```
77
78## Table of Contents
79
80- [Installation](#installation)
81 - [Redux Toolkit](#redux-toolkit)
82 - [Standalone](#standalone)
83- [Basic Usage](#basic-usage)
84- [API](#api)
85 - [createSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?)](#createselectorinputselectors--inputselectors-resultfunc-selectoroptions)
86 - [defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck)](#defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck)
87 - [createSelectorCreator(memoize, ...memoizeOptions)](#createselectorcreatormemoize-memoizeoptions)
88 - [Customize `equalityCheck` for `defaultMemoize`](#customize-equalitycheck-for-defaultmemoize)
89 - [Use memoize function from Lodash for an unbounded cache](#use-memoize-function-from-lodash-for-an-unbounded-cache)
90 - [createStructuredSelector({inputSelectors}, selectorCreator = createSelector)](#createstructuredselectorinputselectors-selectorcreator--createselector)
91- [FAQ](#faq)
92 - [Q: Why isn’t my selector recomputing when the input state changes?](#q-why-isnt-my-selector-recomputing-when-the-input-state-changes)
93 - [Q: Why is my selector recomputing when the input state stays the same?](#q-why-is-my-selector-recomputing-when-the-input-state-stays-the-same)
94 - [Q: Can I use Reselect without Redux?](#q-can-i-use-reselect-without-redux)
95 - [Q: How do I create a selector that takes an argument?](#q-how-do-i-create-a-selector-that-takes-an-argument)
96 - [Q: The default memoization function is no good, can I use a different one?](#q-the-default-memoization-function-is-no-good-can-i-use-a-different-one)
97 - [Q: How do I test a selector?](#q-how-do-i-test-a-selector)
98 - [Q: Can I share a selector across multiple component instances?](#q-can-i-share-a-selector-across-multiple-component-instances)
99 - [Q: Are there TypeScript Typings?](#q-are-there-typescript-typings)
100 - [Q: How can I make a curried selector?](#q-how-can-i-make-a-curried-selector)
101- [Related Projects](#related-projects)
102 - [re-reselect](#re-reselect)
103 - [reselect-tools](#reselect-tools)
104 - [reselect-debugger](#reselect-debugger)
105- [License](#license)
106- [Prior Art and Inspiration](#prior-art-and-inspiration)
107
108## API
109
110### createSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?)
111
112Accepts one or more "input selectors" (either as separate arguments or a single array), a single "output selector" / "result function", and an optional options object, and generates a memoized selector function.
113
114When the selector is called, each input selector will be called with all of the provided arguments. The extracted values are then passed as separate arguments to the output selector, which should calculate and return a final result. The inputs and result are cached for later use.
115
116If the selector is called again with the same arguments, the previously cached result is returned instead of recalculating a new result.
117
118`createSelector` determines if the value returned by an input-selector has changed between calls using reference equality (`===`). Inputs to selectors created with `createSelector` should be immutable.
119
120By default, selectors created with `createSelector` have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector. This can be customized by passing a `selectorOptions` object with a `memoizeOptions` field containing options for the built-in `defaultMemoize` memoization function .
121
122```js
123const selectValue = createSelector(
124 state => state.values.value1,
125 state => state.values.value2,
126 (value1, value2) => value1 + value2
127)
128
129// You can also pass an array of selectors
130const selectTotal = createSelector(
131 [state => state.values.value1, state => state.values.value2],
132 (value1, value2) => value1 + value2
133)
134
135// Selector behavior can be customized
136const customizedSelector = createSelector(
137 state => state.a,
138 state => state.b,
139 (a, b) => a + b,
140 {
141 // New in 4.1: Pass options through to the built-in `defaultMemoize` function
142 memoizeOptions: {
143 equalityCheck: (a, b) => a === b,
144 maxSize: 10,
145 resultEqualityCheck: shallowEqual
146 }
147 }
148)
149```
150
151Selectors are typically called with a Redux `state` value as the first argument, and the input selectors extract pieces of the `state` object for use in calculations. However, it's also common to want to pass additional arguments, such as a value to filter by. Since input selectors are given all arguments, they can extract the additional arguments and pass them to the output selector:
152
153```js
154const selectItemsByCategory = createSelector(
155 [
156 // Usual first input - extract value from `state`
157 state => state.items,
158 // Take the second arg, `category`, and forward to the output selector
159 (state, category) => category
160 ],
161 // Output selector gets (`items, category)` as args
162 (items, category) => items.filter(item => item.category === category)
163)
164```
165
166### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck)
167
168`defaultMemoize` memoizes the function passed in the func parameter. It is the standard memoize function used by `createSelector`.
169
170`defaultMemoize` has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (new in 4.1).
171
172`defaultMemoize` determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using reference equality:
173
174```js
175function defaultEqualityCheck(previousVal, currentVal) {
176 return currentVal === previousVal
177}
178```
179
180As of Reselect 4.1, `defaultMemoize` also accepts an options object as its first argument instead of `equalityCheck`. The options object may contain:
181
182```ts
183interface DefaultMemoizeOptions {
184 equalityCheck?: EqualityFn
185 resultEqualityCheck?: EqualityFn
186 maxSize?: number
187}
188```
189
190Available options are:
191
192- `equalityCheck`: used to compare the individual arguments of the provided calculation function
193- `resultEqualityCheck`: if provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This address the common `todos.map(todo => todo.id)` use case, where an update to another field in the original data causes a recalculate due to changed references, but the output is still effectively the same.
194- `maxSize`: the cache size for the selector. If `maxSize` is greater than 1, the selector will use an LRU cache internally
195
196The returned memoized function will have a `.clearCache()` method attached.
197
198`defaultMemoize` can also be used with `createSelectorCreator` to create a new selector factory that always has the same settings for each selector.
199
200### createSelectorCreator(memoize, ...memoizeOptions)
201
202`createSelectorCreator` can be used to make a customized version of `createSelector`.
203
204The `memoize` argument is a memoization function to replace `defaultMemoize`.
205
206The `...memoizeOptions` rest parameters are zero or more configuration options to be passed to `memoizeFunc`. The selectors `resultFunc` is passed as the first argument to `memoize` and the `memoizeOptions` are passed as the second argument onwards:
207
208```js
209const customSelectorCreator = createSelectorCreator(
210 customMemoize, // function to be used to memoize resultFunc
211 option1, // option1 will be passed as second argument to customMemoize
212 option2, // option2 will be passed as third argument to customMemoize
213 option3 // option3 will be passed as fourth argument to customMemoize
214)
215
216const customSelector = customSelectorCreator(
217 input1,
218 input2,
219 resultFunc // resultFunc will be passed as first argument to customMemoize
220)
221```
222
223Internally `customSelector` calls the memoize function as follows:
224
225```js
226customMemoize(resultFunc, option1, option2, option3)
227```
228
229Here are some examples of how you might use `createSelectorCreator`:
230
231#### Customize `equalityCheck` for `defaultMemoize`
232
233```js
234import { createSelectorCreator, defaultMemoize } from 'reselect'
235import isEqual from 'lodash.isequal'
236
237// create a "selector creator" that uses lodash.isequal instead of ===
238const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
239
240// use the new "selector creator" to create a selector
241const selectSum = createDeepEqualSelector(
242 state => state.values.filter(val => val < 5),
243 values => values.reduce((acc, val) => acc + val, 0)
244)
245```
246
247#### Use memoize function from Lodash for an unbounded cache
248
249```js
250import { createSelectorCreator } from 'reselect'
251import memoize from 'lodash.memoize'
252
253let called = 0
254const hashFn = (...args) =>
255 args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '')
256const customSelectorCreator = createSelectorCreator(memoize, hashFn)
257const selector = customSelectorCreator(
258 state => state.a,
259 state => state.b,
260 (a, b) => {
261 called++
262 return a + b
263 }
264)
265```
266
267### createStructuredSelector({inputSelectors}, selectorCreator = createSelector)
268
269`createStructuredSelector` is a convenience function for a common pattern that arises when using Reselect. The selector passed to a `connect` decorator often just takes the values of its input-selectors and maps them to keys in an object:
270
271```js
272const selectA = state => state.a
273const selectB = state => state.b
274
275// The result function in the following selector
276// is simply building an object from the input selectors
277const structuredSelector = createSelector(selectA, selectB, (a, b) => ({
278 a,
279 b
280}))
281```
282
283`createStructuredSelector` takes an object whose properties are input-selectors and returns a structured selector. The structured selector returns an object with the same keys as the `inputSelectors` argument, but with the selectors replaced with their values.
284
285```js
286const selectA = state => state.a
287const selectB = state => state.b
288
289const structuredSelector = createStructuredSelector({
290 x: selectA,
291 y: selectB
292})
293
294const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 }
295```
296
297Structured selectors can be nested:
298
299```js
300const nestedSelector = createStructuredSelector({
301 subA: createStructuredSelector({
302 selectorA,
303 selectorB
304 }),
305 subB: createStructuredSelector({
306 selectorC,
307 selectorD
308 })
309})
310```
311
312## FAQ
313
314### Q: Why isn’t my selector recomputing when the input state changes?
315
316A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using Redux). For example, a selector created with `createSelector` will not work with a state update function that mutates an existing object instead of creating a new one each time. `createSelector` uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using Redux, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html).
317
318The following example defines a simple selector that determines if the first todo item in an array of todos has been completed:
319
320```js
321const selectIsFirstTodoComplete = createSelector(
322 state => state.todos[0],
323 todo => todo && todo.completed
324)
325```
326
327The following state update function **will not** work with `selectIsFirstTodoComplete`:
328
329```js
330export default function todos(state = initialState, action) {
331 switch (action.type) {
332 case COMPLETE_ALL:
333 const areAllMarked = state.every(todo => todo.completed)
334 // BAD: mutating an existing object
335 return state.map(todo => {
336 todo.completed = !areAllMarked
337 return todo
338 })
339
340 default:
341 return state
342 }
343}
344```
345
346The following state update function **will** work with `selectIsFirstTodoComplete`:
347
348```js
349export default function todos(state = initialState, action) {
350 switch (action.type) {
351 case COMPLETE_ALL:
352 const areAllMarked = state.every(todo => todo.completed)
353 // GOOD: returning a new object each time with Object.assign
354 return state.map(todo =>
355 Object.assign({}, todo, {
356 completed: !areAllMarked
357 })
358 )
359
360 default:
361 return state
362 }
363}
364```
365
366If you are not using Redux and have a requirement to work with mutable data, you can use `createSelectorCreator` to replace the default memoization function and/or use a different equality check function. See [here](#use-memoize-function-from-lodash-for-an-unbounded-cache) and [here](#customize-equalitycheck-for-defaultmemoize) for examples.
367
368### Q: Why is my selector recomputing when the input state stays the same?
369
370A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using Redux). For example, a selector created with `createSelector` that recomputes unexpectedly may be receiving a new object on each update whether the values it contains have changed or not. `createSelector` uses an identity check (`===`) to detect that an input has changed, so returning a new object on each update means that the selector will recompute on each update.
371
372```js
373import { REMOVE_OLD } from '../constants/ActionTypes'
374
375const initialState = [
376 {
377 text: 'Use Redux',
378 completed: false,
379 id: 0,
380 timestamp: Date.now()
381 }
382]
383
384export default function todos(state = initialState, action) {
385 switch (action.type) {
386 case REMOVE_OLD:
387 return state.filter(todo => {
388 return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now()
389 })
390 default:
391 return state
392 }
393}
394```
395
396The following selector is going to recompute every time REMOVE_OLD is invoked because Array.filter always returns a new object. However, in the majority of cases the REMOVE_OLD action will not change the list of todos so the recomputation is unnecessary.
397
398```js
399import { createSelector } from 'reselect'
400
401const todosSelector = state => state.todos
402
403export const selectVisibleTodos = createSelector(
404 todosSelector,
405 (todos) => {
406 ...
407 }
408)
409```
410
411You can eliminate unnecessary recomputations by returning a new object from the state update function only when a deep equality check has found that the list of todos has actually changed:
412
413```js
414import { REMOVE_OLD } from '../constants/ActionTypes'
415import isEqual from 'lodash.isequal'
416
417const initialState = [
418 {
419 text: 'Use Redux',
420 completed: false,
421 id: 0,
422 timestamp: Date.now()
423 }
424]
425
426export default function todos(state = initialState, action) {
427 switch (action.type) {
428 case REMOVE_OLD:
429 const updatedState = state.filter(todo => {
430 return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now()
431 })
432 return isEqual(updatedState, state) ? state : updatedState
433 default:
434 return state
435 }
436}
437```
438
439Alternatively, the default `equalityCheck` function in the selector can be replaced by a deep equality check:
440
441```js
442import { createSelectorCreator, defaultMemoize } from 'reselect'
443import isEqual from 'lodash.isequal'
444
445const selectTodos = state => state.todos
446
447// create a "selector creator" that uses lodash.isequal instead of ===
448const createDeepEqualSelector = createSelectorCreator(
449 defaultMemoize,
450 isEqual
451)
452
453// use the new "selector creator" to create a selector
454const mySelector = createDeepEqualSelector(
455 todosSelector,
456 (todos) => {
457 ...
458 }
459)
460```
461
462Always check that the cost of an alternative `equalityCheck` function or deep equality check in the state update function is not greater than the cost of recomputing every time. If recomputing every time does work out to be the cheaper option, it may be that for this case Reselect is not giving you any benefit over passing a plain `mapStateToProps` function to `connect`.
463
464### Q: Can I use Reselect without Redux?
465
466A: Yes. Reselect has no dependencies on any other package, so although it was designed to be used with Redux it can be used independently. It can be used with any plain JS data, such as typical React state values, as long as that data is being updated immutably.
467
468### Q: How do I create a selector that takes an argument?
469
470As shown in the API reference section above, provide input selectors that extract the arguments and forward them to the output selector for calculation:
471
472```js
473const selectItemsByCategory = createSelector(
474 [
475 // Usual first input - extract value from `state`
476 state => state.items,
477 // Take the second arg, `category`, and forward to the output selector
478 (state, category) => category
479 ],
480 // Output selector gets (`items, category)` as args
481 (items, category) => items.filter(item => item.category === category)
482)
483```
484
485### Q: The default memoization function is no good, can I use a different one?
486
487A: We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize).
488
489### Q: How do I test a selector?
490
491A: For a given input, a selector should always produce the same output. For this reason they are simple to unit test.
492
493```js
494const selector = createSelector(
495 state => state.a,
496 state => state.b,
497 (a, b) => ({
498 c: a * 2,
499 d: b * 3
500 })
501)
502
503test('selector unit test', () => {
504 assert.deepEqual(selector({ a: 1, b: 2 }), { c: 2, d: 6 })
505 assert.deepEqual(selector({ a: 2, b: 3 }), { c: 4, d: 9 })
506})
507```
508
509It may also be useful to check that the memoization function for a selector works correctly with the state update function (i.e. the reducer if you are using Redux). Each selector has a `recomputations` method that will return the number of times it has been recomputed:
510
511```js
512suite('selector', () => {
513 let state = { a: 1, b: 2 }
514
515 const reducer = (state, action) => ({
516 a: action(state.a),
517 b: action(state.b)
518 })
519
520 const selector = createSelector(
521 state => state.a,
522 state => state.b,
523 (a, b) => ({
524 c: a * 2,
525 d: b * 3
526 })
527 )
528
529 const plusOne = x => x + 1
530 const id = x => x
531
532 test('selector unit test', () => {
533 state = reducer(state, plusOne)
534 assert.deepEqual(selector(state), { c: 4, d: 9 })
535 state = reducer(state, id)
536 assert.deepEqual(selector(state), { c: 4, d: 9 })
537 assert.equal(selector.recomputations(), 1)
538 state = reducer(state, plusOne)
539 assert.deepEqual(selector(state), { c: 6, d: 12 })
540 assert.equal(selector.recomputations(), 2)
541 })
542})
543```
544
545Additionally, selectors keep a reference to the last result function as `.resultFunc`. If you have selectors composed of many other selectors this can help you test each selector without coupling all of your tests to the shape of your state.
546
547For example if you have a set of selectors like this:
548
549**selectors.js**
550
551```js
552export const selectFirst = createSelector( ... )
553export const selectSecond = createSelector( ... )
554export const selectThird = createSelector( ... )
555
556export const myComposedSelector = createSelector(
557 selectFirst,
558 selectSecond,
559 selectThird,
560 (first, second, third) => first * second < third
561)
562```
563
564And then a set of unit tests like this:
565
566**test/selectors.js**
567
568```js
569// tests for the first three selectors...
570test("selectFirst unit test", () => { ... })
571test("selectSecond unit test", () => { ... })
572test("selectThird unit test", () => { ... })
573
574// We have already tested the previous
575// three selector outputs so we can just call `.resultFunc`
576// with the values we want to test directly:
577test("myComposedSelector unit test", () => {
578 // here instead of calling selector()
579 // we just call selector.resultFunc()
580 assert(myComposedSelector.resultFunc(1, 2, 3), true)
581 assert(myComposedSelector.resultFunc(2, 2, 1), false)
582})
583```
584
585Finally, each selector has a `resetRecomputations` method that sets
586recomputations back to 0. The intended use is for a complex selector that may
587have many independent tests and you don't want to manually manage the
588computation count or create a "dummy" selector for each test.
589
590### Q: Can I share a selector across multiple component instances?
591
592A: Yes, although it requires some planning.
593
594As of Reselect 4.1, you can create a selector with a cache size greater than one by passing in a `maxSize` option under `memoizeOptions` for use with the built-in `defaultMemoize`.
595
596Otherwise, selectors created using `createSelector` only have a cache size of one. This can make them unsuitable for sharing across multiple instances if the arguments to the selector are different for each instance of the component. There are a couple of ways to get around this:
597
598- Create a factory function which returns a new selector for each instance of the component. This can be called in a React component inside the `useMemo` hook to generate a unique selector instance per component.
599- Create a custom selector with a cache size greater than one using `createSelectorCreator`
600
601### Q: Are there TypeScript Typings?
602
603A: Yes! Reselect is now written in TS itself, so they should Just Work™.
604
605### Q: I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite`
606
607A: This can often occur with deeply recursive types, which occur in this library. Please see [this
608comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as
609relating to nested selectors.
610
611### Q: How can I make a [curried](https://github.com/hemanth/functional-programming-jargon#currying) selector?
612
613A: Try these [helper functions](https://github.com/reduxjs/reselect/issues/159#issuecomment-238724788) courtesy of [MattSPalmer](https://github.com/MattSPalmer)
614
615## Related Projects
616
617### [re-reselect](https://github.com/toomuchdesign/re-reselect)
618
619Enhances Reselect selectors by wrapping `createSelector` and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function.
620
621Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments.
622
623### [reselect-tools](https://github.com/skortchmark9/reselect-tools)
624
625[Chrome extension](https://chrome.google.com/webstore/detail/reselect-devtools/cjmaipngmabglflfeepmdiffcijhjlbb?hl=en) and [companion lib](https://github.com/skortchmark9/reselect-tools) for debugging selectors.
626
627- Measure selector recomputations across the app and identify performance bottlenecks
628- Check selector dependencies, inputs, outputs, and recomputations at any time with the chrome extension
629- Statically export a JSON representation of your selector graph for further analysis
630
631### [reselect-debugger](https://github.com/vlanemcev/reselect-debugger-flipper)
632
633[Flipper plugin](https://github.com/vlanemcev/flipper-plugin-reselect-debugger) and [and the connect app](https://github.com/vlanemcev/reselect-debugger-flipper) for debugging selectors in **React Native Apps**.
634
635Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper.
636
637- Selectors Recomputations count in live time across the App for identify performance bottlenecks
638- Highlight most recomputed selectors
639- Dependency Graph
640- Search by Selectors Graph
641- Selectors Inputs
642- Selectors Output (In case if selector not dependent from external arguments)
643- Shows "Not Memoized (NM)" selectors
644
645## License
646
647MIT
648
649## Prior Art and Inspiration
650
651Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nuclear-js.git), [subscriptions](https://github.com/Day8/re-frame#just-a-read-only-cursor) in [re-frame](https://github.com/Day8/re-frame) and this [proposal](https://github.com/reduxjs/redux/pull/169) from [speedskater](https://github.com/speedskater).
652
653[build-badge]: https://img.shields.io/github/workflow/status/reduxjs/redux-thunk/Tests
654[build]: https://github.com/reduxjs/reselect/actions/workflows/build-and-test-types.yml
655[npm-badge]: https://img.shields.io/npm/v/reselect.svg?style=flat-square
656[npm]: https://www.npmjs.org/package/reselect
657[coveralls-badge]: https://img.shields.io/coveralls/reduxjs/reselect/master.svg?style=flat-square
658[coveralls]: https://coveralls.io/github/reduxjs/reselect