Skip to content

Instantly share code, notes, and snippets.

@markerikson
Last active April 14, 2018 21:02
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save markerikson/3fe0054d16a39b637e2b894755f73586 to your computer and use it in GitHub Desktop.
Save markerikson/3fe0054d16a39b637e2b894755f73586 to your computer and use it in GitHub Desktop.
Reselect selectors and memoization explanation

[9:39 PM] suark: A selector is a function that takes only 1 param which is state and it returns data.
[9:40 PM] maladr0it: in my case it can't function with only that information
[9:40 PM] maladr0it: so i should perhaps rethink
[9:40 PM] maladr0it: it needs to know what chat to get messages for
[9:40 PM] acemarke: @suark: a selector can take any number of arguments
[9:40 PM] acemarke: both in general, and with Reselect specifically
[9:41 PM] suark: I thought createselect returns a function that only takes state
[9:41 PM] acemarke: nope
[9:41 PM] acemarke: in fact, it calls all "input selectors" with all the arguments you pass in
[9:41 PM] maladr0it: only taking state sounds very limiting.. perhaps i should read about what selectors actually are used for
[9:42 PM] suark: Hmm that much we just how we use it, then.
[9:42 PM] acemarke:

const selectItems = state => state.items;  
const selectItemId = (state, itemId) => itemId;  
  
const selectItemById = createSelector(  
    [selectItems, selectItemId],  
    (items, itemId) => items[itemId]  
);  

[9:42 PM] acemarke: const item = selectItemById(state, 42);
[9:42 PM] suark: So what happens to the 42?
[9:43 PM] acemarke: internally, it does something like this (but more generically):
[9:43 PM] maladr0it: how could you accomplish that with a function that only takes state?
[9:43 PM] acemarke:

const firstArg = selectItems(state, 42);  
const secondArg = selectItemId(state, 42);  
  
const result = outputSelector(firstArg, secondArg);  
return result;  

[9:44 PM] suark: If I need pass special params to a selector then we always write a function that out puts a new function
[9:44 PM] maladr0it: isn't that really inefficient?
[9:44 PM] acemarke: there's two small things to note about passing extra params to selectors
[9:44 PM] suark: Naw
[9:44 PM] maladr0it: not that performance is everything
[9:44 PM] acemarke: one is that the selector memoizes on all of the inputs
[9:44 PM] maladr0it: but taking 1 extra argument vs maknig a function dynamically every time
[9:45 PM] acemarke: so if you called it with (state, 42), and then (state, 43), those are different inputs
[9:45 PM] acemarke: it'll return a result, but can't reuse the last saved result
[9:45 PM] suark: Oh so you could usethat to help control the memorization, I kinda of get that
[9:45 PM] acemarke: second, because all the input selectors are called with the same arguments, they all need to accept the same "argument shape"
[9:45 PM] acemarke: example
[9:46 PM] suark: That makes sense
[9:46 PM] acemarke:

const selectItems = state => state.items;  
const selectItemId = (state, itemId) => itemId;  
const selectOtherField (state, someObject) => someObject.someField;  
  
const selectItemById = createSelector(  
    [selectItems, selectItemId, selectOtherField],  
    (items, itemId, someField) => items[itemId]  
);  

[9:46 PM] acemarke: in this case, selectItemId expects that its second argument will be some simple value
[9:46 PM] acemarke: while selectOtherField is expecting it to be an object
[9:47 PM] acemarke: if you pass in a number or string or something as the second param, selectOtherField will break
[9:47 PM] suark: Makes sense
[9:47 PM] maladr0it: hm i got some reading to do
[9:47 PM] acemarke: https://github.com/markerikson/react-redux-links/blob/master/redux-reducers-selectors.md#selectors-and-normalization
GitHub
[9:47 PM] maladr0it: my app doens't have search or filtering atm so selectors dont' seem important atm
[9:47 PM] acemarke: sure
[9:47 PM] suark: I think the way we have been strictly making it so selectors only accept state has been working well for us, overall. But it does give me some new info to ponder on
[9:48 PM] acemarke: the main reasons to use selectors are encapsulation and performance
[9:48 PM] maladr0it: most of the data is strictly serial
[9:48 PM] acemarke: for example, let's say that part of your store is state.some.deeply.nested.field
[9:48 PM] acemarke: and you use that in many different mapState functions
[9:48 PM] suark: (I love this example :))
[9:48 PM] acemarke: then you decide it should actually be located at state.some.other.location
[9:48 PM] acemarke: now you have to go find every place that's used and change it
[9:49 PM] acemarke: but, if you had a selector function that was responsible for reading that value
[9:49 PM] acemarke: and you used that selector in every mapState function already
[9:49 PM] acemarke: now you only need to change the reducer and the selector
[9:49 PM] acemarke: and all your mapState functions never know you changed the value's location
[9:49 PM] acemarke: is that important for every app? no
[9:49 PM] acemarke: is it a good practice? usually, yes
[9:49 PM] suark: 💦💦
[9:49 PM] acemarke: as for performance
[9:50 PM] acemarke: let's say that one of your components requires a very expensive filtering/sorting/transformation step
[9:50 PM] acemarke: and to start with, your mapState function looks like
[9:51 PM] acemarke:

const mapState = (state) => {  
    const {someData} = state;  
  
    const filteredData = expensiveFiltering(someData);  
    const sortedData = expensiveSorting(filteredData);  
    const transformedData = expensiveTransformation(sortedData);  
  
    return {data : transformedData};  
}  

[9:51 PM] acemarke: right now, that expensive logic will re-run for every dispatched action that updates the store
[9:51 PM] acemarke: even if the store state that was changed was in a completely different location
[9:51 PM] acemarke: what we really want is to only re-run those steps if state.someData has changed
[9:51 PM] acemarke: which is where "memoization" comes in
[9:52 PM] acemarke: which says "if you call this with the exact same inputs as before, I can skip doing the work and hand back the result from last time"
[9:52 PM] acemarke: so...
[9:52 PM] acemarke: and the Reselect library helps you create memoized selector functions
[9:54 PM] acemarke:

const selectFilteredSortedTransformedData = createSelector(  
    state => state.someData,  
    (someData) => {  
         const filteredData = expensiveFiltering(someData);  
         const sortedData = expensiveSorting(filteredData);  
         const transformedData = expensiveTransformation(sortedData);  
  
         return transformedData;  
    }  
)  
  
const mapState = (state) => {  
    const transformedData = selectFilteredSortedTransformedData (state);  
  
    return {data : transformedData};  
}  

[9:54 PM] acemarke: and now the filtering/sorting/transforming step will only re-run if state.someData is a new reference
[9:55 PM] acemarke: and on top of that, if state.someData hasn't changed, then the return value of transformedData wlil stay the same
[9:55 PM] acemarke: which means connect will look at the returned {data} field, see that it's the same as before, and skip re-rendering the component, because none of the values from mapState have changed
[9:55 PM] acemarke: so if you want to fully optimize perf, that's how you do it(edited)
[9:56 PM] acemarke: do you need to do this right away? no(edited)
[9:56 PM] acemarke: is it a good practice for the long term? yes
[9:56 PM] maladr0it: hmm this is very cool. i'll look into it when i add expensive filtering like search
[9:56 PM] suark: When we added selectors for our main app, it made massive performance improvement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment