- Notifications
You must be signed in to change notification settings - Fork0
vlanemcev/reselect
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Simple “selector” library for Redux (and others) inspired by getters inNuclearJS,subscriptions inre-frame and thisproposal fromspeedskater.
- Selectors can compute derived data, allowing Redux to store the minimal possible state.
- Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
- Selectors are composable. They can be used as input to other selectors.
You can play around with the followingexample inthis codepen:
import{createSelector}from'reselect'constshopItemsSelector=state=>state.shop.itemsconsttaxPercentSelector=state=>state.shop.taxPercentconstsubtotalSelector=createSelector(shopItemsSelector,items=>items.reduce((subtotal,item)=>subtotal+item.value,0))consttaxSelector=createSelector(subtotalSelector,taxPercentSelector,(subtotal,taxPercent)=>subtotal*(taxPercent/100))consttotalSelector=createSelector(subtotalSelector,taxSelector,(subtotal,tax)=>({total:subtotal+tax}))constexampleState={shop:{taxPercent:8,items:[{name:'apple',value:1.20},{name:'orange',value:0.95},]}}console.log(subtotalSelector(exampleState))// 2.15console.log(taxSelector(exampleState))// 0.172console.log(totalSelector(exampleState))// { total: 2.322 }
- Why isn't my selector recomputing when the input state changes?
- Why is my selector recomputing when the input state stays the same?
- Can I use Reselect without Redux?
- The default memoization function is no good, can I use a different one?
- How do I test a selector?
- How do I create a selector that takes an argument?
- How do I use Reselect with Immutable.js?
- Can I share a selector across multiple component instances?
- Are there TypeScript typings?
- How can I make a curried selector?
npm install reselectIf you prefer a video tutorial, you can find onehere.
The examples in this section are based on theRedux Todos List example.
import{connect}from'react-redux'import{toggleTodo}from'../actions'importTodoListfrom'../components/TodoList'constgetVisibleTodos=(todos,filter)=>{switch(filter){case'SHOW_ALL':returntodoscase'SHOW_COMPLETED':returntodos.filter(t=>t.completed)case'SHOW_ACTIVE':returntodos.filter(t=>!t.completed)}}constmapStateToProps=(state)=>{return{todos:getVisibleTodos(state.todos,state.visibilityFilter)}}constmapDispatchToProps=(dispatch)=>{return{onTodoClick:(id)=>{dispatch(toggleTodo(id))}}}constVisibleTodoList=connect(mapStateToProps,mapDispatchToProps)(TodoList)exportdefaultVisibleTodoList
In the above example,mapStateToProps callsgetVisibleTodos to calculatetodos. This works great, but there is a drawback:todos is calculated every time the state tree is updated. If the state tree is large, or the calculation expensive, repeating the calculation on every update may cause performance problems. Reselect can help to avoid these unnecessary recalculations.
We would like to replacegetVisibleTodos with a memoized selector that recalculatestodos when the value ofstate.todos orstate.visibilityFilter changes, but not when changes occur in other (unrelated) parts of the state tree.
Reselect provides a functioncreateSelector for creating memoized selectors.createSelector takes an array of input-selectors and a transform function as its arguments. If the Redux state tree is mutated in a way that causes the value of an input-selector to change, the selector will call its transform function with the values of the input-selectors as arguments and return the result. If the values of the input-selectors are the same as the previous call to the selector, it will return the previously computed value instead of calling the transform function.
Let's define a memoized selector namedgetVisibleTodos to replace the non-memoized version above:
import{createSelector}from'reselect'constgetVisibilityFilter=(state)=>state.visibilityFilterconstgetTodos=(state)=>state.todosexportconstgetVisibleTodos=createSelector([getVisibilityFilter,getTodos],(visibilityFilter,todos)=>{switch(visibilityFilter){case'SHOW_ALL':returntodoscase'SHOW_COMPLETED':returntodos.filter(t=>t.completed)case'SHOW_ACTIVE':returntodos.filter(t=>!t.completed)}})
In the example above,getVisibilityFilter andgetTodos are input-selectors. They are created as ordinary non-memoized selector functions because they do not transform the data they select.getVisibleTodos on the other hand is a memoized selector. It takesgetVisibilityFilter andgetTodos as input-selectors, and a transform function that calculates the filtered todos list.
A memoized selector can itself be an input-selector to another memoized selector. Here isgetVisibleTodos being used as an input-selector to a selector that further filters the todos by keyword:
constgetKeyword=(state)=>state.keywordconstgetVisibleTodosFilteredByKeyword=createSelector([getVisibleTodos,getKeyword],(visibleTodos,keyword)=>visibleTodos.filter(todo=>todo.text.includes(keyword)))
If you are usingReact Redux, you can call selectors as regular functions insidemapStateToProps():
import{connect}from'react-redux'import{toggleTodo}from'../actions'importTodoListfrom'../components/TodoList'import{getVisibleTodos}from'../selectors'constmapStateToProps=(state)=>{return{todos:getVisibleTodos(state)}}constmapDispatchToProps=(dispatch)=>{return{onTodoClick:(id)=>{dispatch(toggleTodo(id))}}}constVisibleTodoList=connect(mapStateToProps,mapDispatchToProps)(TodoList)exportdefaultVisibleTodoList
This section introduces a hypothetical extension to our app that allows it to support multiple Todo Lists. Please note that a full implementation of this extension requires changes to the reducers, components, actions etc. that aren’t directly relevant to the topics discussed and have been omitted for brevity.
So far we have only seen selectors receive the Redux store state as an argument, but a selector can receive props too.
Here is anApp component that renders threeVisibleTodoList component instances, each of which has alistId prop:
importReactfrom'react'importFooterfrom'./Footer'importAddTodofrom'../containers/AddTodo'importVisibleTodoListfrom'../containers/VisibleTodoList'constApp=()=>(<div><VisibleTodoListlistId="1"/><VisibleTodoListlistId="2"/><VisibleTodoListlistId="3"/></div>)
EachVisibleTodoList container should select a different slice of the state depending on the value of thelistId prop, so let’s modifygetVisibilityFilter andgetTodos to accept a props argument:
import{createSelector}from'reselect'constgetVisibilityFilter=(state,props)=>state.todoLists[props.listId].visibilityFilterconstgetTodos=(state,props)=>state.todoLists[props.listId].todosconstgetVisibleTodos=createSelector([getVisibilityFilter,getTodos],(visibilityFilter,todos)=>{switch(visibilityFilter){case'SHOW_COMPLETED':returntodos.filter(todo=>todo.completed)case'SHOW_ACTIVE':returntodos.filter(todo=>!todo.completed)default:returntodos}})exportdefaultgetVisibleTodos
props can be passed togetVisibleTodos frommapStateToProps:
constmapStateToProps=(state,props)=>{return{todos:getVisibleTodos(state,props)}}
So nowgetVisibleTodos has access toprops, and everything seems to be working fine.
But there is a problem!
Using thegetVisibleTodos selector with multiple instances of theVisibleTodoList container will not correctly memoize:
import{connect}from'react-redux'import{toggleTodo}from'../actions'importTodoListfrom'../components/TodoList'import{getVisibleTodos}from'../selectors'constmapStateToProps=(state,props)=>{return{// WARNING: THE FOLLOWING SELECTOR DOES NOT CORRECTLY MEMOIZEtodos:getVisibleTodos(state,props)}}constmapDispatchToProps=(dispatch)=>{return{onTodoClick:(id)=>{dispatch(toggleTodo(id))}}}constVisibleTodoList=connect(mapStateToProps,mapDispatchToProps)(TodoList)exportdefaultVisibleTodoList
A selector created withcreateSelector has a cache size of 1 and only returns the cached value when its set of arguments is the same as its previous set of arguments. If we alternate between rendering<VisibleTodoList listId="1" /> and<VisibleTodoList listId="2" />, the shared selector will alternate between receiving{listId: 1} and{listId: 2} as itsprops argument. This will cause the arguments to be different on each call, so the selector will always recompute instead of returning the cached value. We’ll see how to overcome this limitation in the next section.
The examples in this section require React Redux v4.3.0 or greater
An alternative approach can be found inre-reselect
To share a selector across multipleVisibleTodoList instances while passing inpropsand retaining memoization, each instance of the component needs its own private copy of the selector.
Let’s create a function namedmakeGetVisibleTodos that returns a new copy of thegetVisibleTodos selector each time it is called:
import{createSelector}from'reselect'constgetVisibilityFilter=(state,props)=>state.todoLists[props.listId].visibilityFilterconstgetTodos=(state,props)=>state.todoLists[props.listId].todosconstmakeGetVisibleTodos=()=>{returncreateSelector([getVisibilityFilter,getTodos],(visibilityFilter,todos)=>{switch(visibilityFilter){case'SHOW_COMPLETED':returntodos.filter(todo=>todo.completed)case'SHOW_ACTIVE':returntodos.filter(todo=>!todo.completed)default:returntodos}})}exportdefaultmakeGetVisibleTodos
We also need a way to give each instance of a container access to its own private selector. ThemapStateToProps argument ofconnect can help with this.
If themapStateToProps argument supplied toconnect returns a function instead of an object, it will be used to create an individualmapStateToProps function for each instance of the container.
In the example belowmakeMapStateToProps creates a newgetVisibleTodos selector, and returns amapStateToProps function that has exclusive access to the new selector:
constmakeMapStateToProps=()=>{constgetVisibleTodos=makeGetVisibleTodos()constmapStateToProps=(state,props)=>{return{todos:getVisibleTodos(state,props)}}returnmapStateToProps}
If we passmakeMapStateToProps toconnect, each instance of theVisibleTodoList container will get its ownmapStateToProps function with a privategetVisibleTodos selector. Memoization will now work correctly regardless of the render order of theVisibleTodoList containers.
import{connect}from'react-redux'import{toggleTodo}from'../actions'importTodoListfrom'../components/TodoList'import{makeGetVisibleTodos}from'../selectors'constmakeMapStateToProps=()=>{constgetVisibleTodos=makeGetVisibleTodos()constmapStateToProps=(state,props)=>{return{todos:getVisibleTodos(state,props)}}returnmapStateToProps}constmapDispatchToProps=(dispatch)=>{return{onTodoClick:(id)=>{dispatch(toggleTodo(id))}}}constVisibleTodoList=connect(makeMapStateToProps,mapDispatchToProps)(TodoList)exportdefaultVisibleTodoList
Takes one or more selectors, or an array of selectors, computes their values and passes them as arguments toresultFunc.
createSelector determines if the value returned by an input-selector has changed between calls using reference equality (===). Inputs to selectors created withcreateSelector should be immutable.
Selectors created withcreateSelector 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.
constmySelector=createSelector(state=>state.values.value1,state=>state.values.value2,(value1,value2)=>value1+value2)// You can also pass an array of selectorsconsttotalSelector=createSelector([state=>state.values.value1,state=>state.values.value2],(value1,value2)=>value1+value2)
It can be useful to access the props of a component from within a selector. When a selector is connected to a component withconnect, the component props are passed as the second argument to the selector:
constabSelector=(state,props)=>state.a*props.b// props only (ignoring state argument)constcSelector=(_,props)=>props.c// state only (props argument omitted as not required)constdSelector=state=>state.dconsttotalSelector=createSelector(abSelector,cSelector,dSelector,(ab,c,d)=>({total:ab+c+d}))
defaultMemoize memoizes the function passed in the func parameter. It is the memoize function used bycreateSelector.
defaultMemoize has a cache size of 1. This means it always recalculates when the value of an argument changes.
defaultMemoize determines if an argument has changed by calling theequalityCheck function. AsdefaultMemoize is designed to be used with immutable data, the defaultequalityCheck function checks for changes using reference equality:
functiondefaultEqualityCheck(previousVal,currentVal){returncurrentVal===previousVal}
defaultMemoize can be used withcreateSelectorCreator tocustomize theequalityCheck function.
createSelectorCreator can be used to make a customized version ofcreateSelector.
Thememoize argument is a memoization function to replacedefaultMemoize.
The...memoizeOptions rest parameters are zero or more configuration options to be passed tomemoizeFunc. The selectorsresultFunc is passed as the first argument tomemoize and thememoizeOptions are passed as the second argument onwards:
constcustomSelectorCreator=createSelectorCreator(customMemoize,// function to be used to memoize resultFuncoption1,// option1 will be passed as second argument to customMemoizeoption2,// option2 will be passed as third argument to customMemoizeoption3// option3 will be passed as fourth argument to customMemoize)constcustomSelector=customSelectorCreator(input1,input2,resultFunc// resultFunc will be passed as first argument to customMemoize)
InternallycustomSelector calls the memoize function as follows:
customMemoize(resultFunc,option1,option2,option3)
Here are some examples of how you might usecreateSelectorCreator:
import{createSelectorCreator,defaultMemoize}from'reselect'importisEqualfrom'lodash.isequal'// create a "selector creator" that uses lodash.isequal instead of ===constcreateDeepEqualSelector=createSelectorCreator(defaultMemoize,isEqual)// use the new "selector creator" to create a selectorconstmySelector=createDeepEqualSelector(state=>state.values.filter(val=>val<5),values=>values.reduce((acc,val)=>acc+val,0))
import{createSelectorCreator}from'reselect'importmemoizefrom'lodash.memoize'letcalled=0consthashFn=(...args)=>args.reduce((acc,val)=>acc+'-'+JSON.stringify(val),'')constcustomSelectorCreator=createSelectorCreator(memoize,hashFn)constselector=customSelectorCreator(state=>state.a,state=>state.b,(a,b)=>{called++returna+b})
createStructuredSelector is a convenience function for a common pattern that arises when using Reselect. The selector passed to aconnect decorator often just takes the values of its input-selectors and maps them to keys in an object:
constmySelectorA=state=>state.aconstmySelectorB=state=>state.b// The result function in the following selector// is simply building an object from the input selectorsconststructuredSelector=createSelector(mySelectorA,mySelectorB,(a,b)=>({ a, b}))
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 theinputSelectors argument, but with the selectors replaced with their values.
constmySelectorA=state=>state.aconstmySelectorB=state=>state.bconststructuredSelector=createStructuredSelector({x:mySelectorA,y:mySelectorB})constresult=structuredSelector({a:1,b:2})// will produce { x: 1, y: 2 }
Structured selectors can be nested:
constnestedSelector=createStructuredSelector({subA:createStructuredSelector({ selectorA, selectorB}),subB:createStructuredSelector({ selectorC, selectorD})})
A: 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 withcreateSelector 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 isalmost certainly a mistake.
The following example defines a simple selector that determines if the first todo item in an array of todos has been completed:
constisFirstTodoCompleteSelector=createSelector(state=>state.todos[0],todo=>todo&&todo.completed)
The following state update functionwill not work withisFirstTodoCompleteSelector:
exportdefaultfunctiontodos(state=initialState,action){switch(action.type){caseCOMPLETE_ALL:constareAllMarked=state.every(todo=>todo.completed)// BAD: mutating an existing objectreturnstate.map(todo=>{todo.completed=!areAllMarkedreturntodo})default:returnstate}}
The following state update functionwill work withisFirstTodoCompleteSelector:
exportdefaultfunctiontodos(state=initialState,action){switch(action.type){caseCOMPLETE_ALL:constareAllMarked=state.every(todo=>todo.completed)// GOOD: returning a new object each time with Object.assignreturnstate.map(todo=>Object.assign({},todo,{completed:!areAllMarked}))default:returnstate}}
If you are not using Redux and have a requirement to work with mutable data, you can usecreateSelectorCreator to replace the default memoization function and/or use a different equality check function. Seehere andhere for examples.
A: 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 withcreateSelector 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.
import{REMOVE_OLD}from'../constants/ActionTypes'constinitialState=[{text:'Use Redux',completed:false,id:0,timestamp:Date.now()}]exportdefaultfunctiontodos(state=initialState,action){switch(action.type){caseREMOVE_OLD:returnstate.filter(todo=>{returntodo.timestamp+30*24*60*60*1000>Date.now()})default:returnstate}}
The 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.
import{createSelector}from'reselect'consttodosSelector=state=>state.todosexportconstvisibleTodosSelector=createSelector(todosSelector,(todos)=>{ ...})
You 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:
import{REMOVE_OLD}from'../constants/ActionTypes'importisEqualfrom'lodash.isequal'constinitialState=[{text:'Use Redux',completed:false,id:0,timestamp:Date.now()}]exportdefaultfunctiontodos(state=initialState,action){switch(action.type){caseREMOVE_OLD:constupdatedState=state.filter(todo=>{returntodo.timestamp+30*24*60*60*1000>Date.now()})returnisEqual(updatedState,state) ?state :updatedStatedefault:returnstate}}
Alternatively, the defaultequalityCheck function in the selector can be replaced by a deep equality check:
import{createSelectorCreator,defaultMemoize}from'reselect'importisEqualfrom'lodash.isequal'consttodosSelector=state=>state.todos// create a "selector creator" that uses lodash.isequal instead of ===constcreateDeepEqualSelector=createSelectorCreator(defaultMemoize,isEqual)// use the new "selector creator" to create a selectorconstmySelector=createDeepEqualSelector(todosSelector,(todos)=>{ ...})
Always check that the cost of an alternativeequalityCheck 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 plainmapStateToProps function toconnect.
A: 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 is currently being used successfully in traditional Flux apps.
If you create selectors using
createSelectormake sure its arguments are immutable.Seehere
A: Keep in mind that selectors can access React props, so if your arguments are (or can be made available as) React props, you can use that functionality.See here for details.
Otherwise, Reselect doesn't have built-in support for creating selectors that accepts arguments, but here are some suggestions for implementing similar functionality...
If the argument is not dynamic you can use a factory function:
constexpensiveItemSelectorFactory=minValue=>{returncreateSelector(shopItemsSelector,items=>items.filter(item=>item.value>minValue))}constsubtotalSelector=createSelector(expensiveItemSelectorFactory(200),items=>items.reduce((acc,item)=>acc+item.value,0))
The general consensushere andover at nuclear-js is that if a selector needs a dynamic argument, then that argument should probably be state in the store. If you decide that you do require a selector with a dynamic argument, then a selector that returns a memoized function may be suitable:
import{createSelector}from'reselect'importmemoizefrom'lodash.memoize'constexpensiveSelector=createSelector(state=>state.items,items=>memoize(minValue=>items.filter(item=>item.value>minValue)))constexpensiveFilter=expensiveSelector(state)constslightlyExpensive=expensiveFilter(100)constveryExpensive=expensiveFilter(1000000)
A: We think it works great for a lot of use cases, but sure. Seethese examples.
A: For a given input, a selector should always produce the same output. For this reason they are simple to unit test.
constselector=createSelector(state=>state.a,state=>state.b,(a,b)=>({c:a*2,d:b*3}))test("selector unit test",()=>{assert.deepEqual(selector({a:1,b:2}),{c:2,d:6})assert.deepEqual(selector({a:2,b:3}),{c:4,d:9})})
It 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 arecomputations method that will return the number of times it has been recomputed:
suite('selector',()=>{letstate={a:1,b:2}constreducer=(state,action)=>({a:action(state.a),b:action(state.b)})constselector=createSelector(state=>state.a,state=>state.b,(a,b)=>({c:a*2,d:b*3}))constplusOne=x=>x+1constid=x=>xtest("selector unit test",()=>{state=reducer(state,plusOne)assert.deepEqual(selector(state),{c:4,d:9})state=reducer(state,id)assert.deepEqual(selector(state),{c:4,d:9})assert.equal(selector.recomputations(),1)state=reducer(state,plusOne)assert.deepEqual(selector(state),{c:6,d:12})assert.equal(selector.recomputations(),2)})})
Additionally, 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.
For example if you have a set of selectors like this:
selectors.js
exportconstfirstSelector=createSelector( ...)exportconstsecondSelector=createSelector( ...)exportconstthirdSelector=createSelector( ...)exportconstmyComposedSelector=createSelector(firstSelector,secondSelector,thirdSelector,(first,second,third)=>first*second<third)
And then a set of unit tests like this:
test/selectors.js
// tests for the first three selectors...test("firstSelector unit test",()=>{ ...})test("secondSelector unit test",()=>{ ...})test("thirdSelector unit test",()=>{ ...})// We have already tested the previous// three selector outputs so we can just call `.resultFunc`// with the values we want to test directly:test("myComposedSelector unit test",()=>{// here instead of calling selector()// we just call selector.resultFunc()assert(myComposedSelector.resultFunc(1,2,3),true)assert(myComposedSelector.resultFunc(2,2,1),false)})
Finally, each selector has aresetRecomputations method that setsrecomputations back to 0. The intended use is for a complex selector that mayhave many independent tests and you don't want to manually manage thecomputation count or create a "dummy" selector for each test.
A: Selectors created withcreateSelector should work just fine with Immutable.js data structures.
If your selector is recomputing and you don't think the state has changed, make sure you are aware of which Immutable.js update methodsalways return a new object and which update methods only return a new objectwhen the collection actually changes.
importImmutablefrom'immutable'letmyMap=Immutable.Map({a:1,b:2,c:3})// set, merge and others only return a new obj when update changes collectionletnewMap=myMap.set('a',1)assert.equal(myMap,newMap)newMap=myMap.merge({'a':1})assert.equal(myMap,newMap)// map, reduce, filter and others always return a new objnewMap=myMap.map(a=>a*1)assert.notEqual(myMap,newMap)
If a selector's input is updated by an operation that always returns a new object, it may be performing unnecessary recomputations. Seehere for a discussion on the pros and cons of using a deep equality check likeImmutable.is to eliminate unnecessary recomputations.
A: Selectors created usingcreateSelector 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:
Create a factory function which returns a new selector for each instance of the component. There is built-in support for factory functions in React Redux v4.3 or higher. Seehere for an example.
Create a custom selector with a cache size greater than one.
A: Yes! They are included and referenced inpackage.json. They should Just Work™.
Q: How can I make acurried selector?
A: Try thesehelper functions courtesy ofMattSPalmer
Enhances Reselect selectors by wrappingcreateSelector and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function.
Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments.
Chrome extension andcompanion lib for debugging selectors.
- Measure selector recomputations across the app and identify performance bottlenecks
- Check selector dependencies, inputs, outputs, and recomputations at any time with the chrome extension
- Statically export a JSON representation of your selector graph for further analysis
Flipper plugin andand the connect app for debugging selectors inReact Native Apps.
Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper.
- Selectors Recomputations count in live time across the App for identify performance bottlenecks
- Highlight most recomputed selectors
- Dependency Graph
- Search by Selectors Graph
- Selectors Inputs
- Selectors Output (In case if selector not dependent from external arguments)
- Shows "Not Memoized (NM)" selectors
Can be useful when doingvery expensive computations on elements of a collection because Reselect might not give you the granularity of caching that you need. Check out the reselect-maps README for examples.
The optimizations in reselect-map only apply in a small number of cases. If you are unsure whether you need it, you don't!
MIT
About
Selector library for Redux
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Languages
- JavaScript54.7%
- TypeScript45.3%