-
Notifications
You must be signed in to change notification settings - Fork 673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to use reselect with Immutable.js? #26
Comments
import {
createSelector
} from 'reselect';
import Immutable from 'immutable';
let state = {},
taskSelector,
activeTaskSelector;
state.tasks = [
{
id: 1,
name: 'foo',
done: true
},
{
id: 2,
name: 'bar',
done: true
},
{
id: 3,
name: 'baz',
done: false
},
];
state = Immutable.fromJS(state);
taskSelector = state => state.get('tasks');
activeTaskSelector = createSelector(
[
taskSelector
],
tasks => {
return tasks.filter(task => task.get('done'));
}
);
console.log( activeTaskSelector(state) === activeTaskSelector(state) ); A common pitfall is to use |
I figure this is the proper way. If I am wrong, someone please correct it. Otherwise, I have created this issue as a reference point. |
@gajus Could you please elaborate why should we use |
@wizardzloy It is explained in the Immutable.js docs, https://facebook.github.io/immutable-js/docs/#/is. var map1 = Immutable.Map({a:1, b:1, c:1});
var map2 = Immutable.Map({a:1, b:1, c:1});
assert(map1 !== map2);
assert(Object.is(map1, map2) === false);
assert(Immutable.is(map1, map2) === true); |
@gajus yes I saw that part of documentation, but I wonder why the simple |
Thats a good point. I will need to do a few tests. |
Immutable.js always returns a new collection when certain operations ( |
@ellbee can you give an example in the context of reselect where that is relevant? |
If the EqualsFn thinks the arguments are different every single time the selector is called, the memoization isn't going to work. I guess it comes down to whether the cost of the deepEquals is greater than the cost of the computation in your selector. |
By the way, I think @wizardzloy is basically right and that |
I think that
In this case, the selector will check
Since the memoized version was returned, any composite filters down the line will also continue to work correctly. In this case, |
@bunkat What if your reducer is updating the store using map, filter, reduce etc.? |
If your reducer updates the store (regardless of how it is done) the value will be different ( If your reducer updates the store with a strictly equivalent but new Immutable instance such that the selector would recalculate wastefully, I think that is a failure of the reducer (it should have just returned state in that case, not an updated state). I think it makes a lot more sense to do the equality check in the reducer (if that is a concern) rather than force all selectors to use I actually can't imagine a scenario where a reducer would systematically be returning a strictly equivalent update as new state. That would mean that changes are happening outside of actions which shouldn't be the case, otherwise you would always know when running the map, filter, reduce would be necessary. |
I totally agree with @bunkat. As for me selectors should be as performant and easy to reason about as possible. So I believe that we should let the |
Yes, you can do a deep equality check in the reducer and not update the state if it hasn't changed, or you can check the state in the action to figure out whether you need to update, or if you prefer the mechanism is there so you can do it in the selector. All I wanted to point out in my original comment is that Also, each selector can be given a different equality function, so there is no need to force every selector to use |
I am still missing a real life example of where |
@gajus Maybe, maybe not :-) I have to say that I've not found any need for it in any real life projects yet. I'll have a play around later and see if I can come up with any really compelling use cases. |
@gajus Here is the sort of situation where Immutable.is() as the equalsFn might possibly be helpful: // state for reducer a:
Immutable.map({
'list1': Immutable.List(),
'list2': Immutable.List(),
'keyword': ''
});
// selectors
const list1$ = state => state.a.get('list1');
const list2$ = state => state.a.get('list2');
const keyword$ = state => state.a.get('keyword');
const filteredList2$ = createSelector(
[list2$, keyword$],
(list2, keyword) => {
return list2.filter(x => x.indexOf(keyword) !== -1);
}
);
const createImmutableSelector = createSelectorCreator(Immutable.is);
const somethingExpensive$ = createImmutableSelector(
[list1$, filteredList2$],
(list1, filteredList2) => {
return //expensive computation
}
);
I'm not saying that this is the best approach, just that
Something I have done which is kind of like this was a background task, running on a timer, which filtered stale items from a list. |
@ellbee I am still not seeing it. import Immutable from 'immutable';
import {
createSelector,
createSelectorCreator
} from 'reselect';
let state,
list1Selector,
list2Selector,
keywordSelector,
filteredList2Selector,
somethingExpensiveSelector,
createImmutableSelector;
// state for reducer a:
state = Immutable.Map({
list1: Immutable.List(),
list2: Immutable.List(),
keyword: ''
});
list1Selector = state => state.get('list1');
list2Selector = state => state.get('list2');
keywordSelector = state => state.get('keyword');
filteredList2Selector = createSelector(
[
list2Selector,
keywordSelector
],
(list2, keyword) => {
console.log('Generating filteredList2Selector...');
return list2
.filter(x => {
return x.indexOf(keyword) !== -1;
});
}
);
createImmutableSelector = createSelectorCreator(Immutable.is);
somethingExpensiveSelector = createImmutableSelector(
[
list1Selector,
filteredList2Selector
],
(list1, filteredList2) => {
console.log('Generating somethingExpensiveSelector...');
return Math.random();
}
);
console.log('filteredList2Selector(state)', filteredList2Selector(state));
console.log('filteredList2Selector(state)', filteredList2Selector(state));
console.log('filteredList2Selector(state)', somethingExpensiveSelector(state));
console.log('filteredList2Selector(state)', somethingExpensiveSelector(state)); If you run this in node, you will get:
You can run this code using |
@gajus you need to modify console.log('filteredList2Selector(state)', filteredList2Selector(state));
console.log('filteredList2Selector(state)', filteredList2Selector(state));
console.log('filteredList2Selector(state)', somethingExpensiveSelector(state));
state.set('keyword', 'not empty string');
console.log('filteredList2Selector(state)', somethingExpensiveSelector(state)); |
@wizardzloy Modify when? |
@gajus between 2 |
But this would never happen... at least in redux domain. |
This is what I meant: import Immutable from 'immutable';
import {
createSelector,
createSelectorCreator
} from 'reselect';
let state,
list1Selector,
list2Selector,
keywordSelector,
filteredList2Selector,
createImmutableSelector;
// state for reducer a:
state = Immutable.Map({
list1: Immutable.List(),
list2: Immutable.List(['hello goodbye', 'hello hello']),
keyword: ''
});
list1Selector = state => state.get('list1');
list2Selector = state => state.get('list2');
keywordSelector = state => state.get('keyword');
filteredList2Selector = createSelector(
[list2Selector, keywordSelector],
(list2, keyword) => {
return list2
.filter(x => {
return x.indexOf(keyword) !== -1;
});
}
);
const somethingExpensive1Selector = createSelector(
[list1Selector, filteredList2Selector],
(list1, filteredList2) => {
console.log('EXPENSIVE COMPUTATION TRIGGERED! Generating for ===...');
return Math.random();
}
);
createImmutableSelector = createSelectorCreator(Immutable.is);
const somethingExpensive2Selector = createImmutableSelector(
[list1Selector, filteredList2Selector],
(list1, filteredList2) => {
console.log('EXPENSIVE COMPUTATION TRIGGERED! Generating for Immutable.is()...');
return Math.random();
}
);
state = state.set('keyword', '');
console.log('expensive1(state)', somethingExpensive1Selector(state));
state = state.set('keyword', 'he');
console.log('expensive1(state)', somethingExpensive1Selector(state));
state = state.set('keyword', 'hell');
console.log('expensive1(state)', somethingExpensive1Selector(state));
state = state.set('keyword', 'helldsfs');
console.log('expensive1(state)', somethingExpensive1Selector(state));
console.log('')
console.log('====================================')
console.log('')
state = state.set('keyword', '');
console.log('expensive2(state)', somethingExpensive2Selector(state));
state = state.set('keyword', 'he');
console.log('expensive2(state)', somethingExpensive2Selector(state));
state = state.set('keyword', 'hell');
console.log('expensive2(state)', somethingExpensive2Selector(state));
state = state.set('keyword', 'helldsfs');
console.log('expensive2(state)', somethingExpensive2Selector(state)); |
Just wondering what the discussion concluded on? If someone is using Immutable.js with Redux, like with redux-immutablejs for example, does it make sense to also use reselect? |
Yes, it does make sense. There is a section in the docs about it here. |
i fail to see why even bother using immutable js with reselect when reselect has already done prop checks for you anyway at the top level, therefore shortcutting any unnecessary rerenders, the docs you link don't actually give a compelling reason to use immutable js with reselect, if anything it just adds complexity for no apparent gain in performance |
@thewillhuang you bring up good points in terms of potentially neglibile performance gains, but a significant benefit I see from using Immutable.js is a consistent way of dealing with values throughout the codebase. Since we already use it in terms of the Redux store structure being an Immutable Map, having the selectors play nicely without converting JSON <-> Immutable types is good. You also get nice, small things like accessing deep properties on an object ( The general idea of why we went with Immutable.js in the first place is an easy API around producing memory-efficient immutable values without worrying about the edge cases of using the ES spread operator and creating copies of deeply-nested objects where just one property has changed. |
@pheuter good points, i was really just thinking if you can simply build a project with reselect or immutable and still get the performance benefits, which is really the main reason to use immutable or reselect in the first place. |
There really is no added complexity in using both immutable.js and reselect and they do work really well together. We use an immutable store with redux to make our reducers and change detection simpler. We use reselect in order to create a facade on top of our store so that the application never needs to know how the store is laid out. We also use reselect to cache computed data. For example, our store has a list of entities that need to be displayed. Instead of the views directly accessing the entities in the store, they access them via reselect. The nice thing is that unknown to the views, the reselect query is actually combining sort and group preferences from the user, current entity list base on the url, and applying filter and searches. So the view just calls the 'entitySelector' and it gets back exactly the right data, which is automatically cached at the highest level possible (i.e. if the sort changes, the entity list is invalidated and recalculated by reselect). |
okay, i see your point. even with react-redux, every prop change, regardless of how small the change is will always force a filter or sort to happen, where as with reselect, there is potential to skip an expensive calculation if the input of a reselected calculation never changes. good points. |
I don't think the docs make it clear enough how easy it is to avoid problems with excessive recomputation, for most cases. First, avoid using filter() & friends in your reducers. Store as little data as possible that will represent your app state; reselect helps you do that. If you absolutely must use them, then sure. But most of the time, don't. You probably either don't need to, or won't be affected much by a couple of false recomputations here and there. These parts of your reducers should be triggered by specific actions, like data coming in off the server or sporadic user-activated or long-interval maintenance operations. (For the example in the docs, you probably wouldn't worry; you don't need to clean up old todos very often.) Second, whenever there's a value in your selectors that's coming from a filter() (or anything that gives a new object every time), extract that portion of the selector into a dependency, and use a simple createSelector to memoize it individually. You don't need Immutable.is at all, you can stick with simple equality checks. For example, this: const filterAndOther = createSelector(
(state) => state.immutableCollection.filter((x) => x > 5), // ALWAYS gives new object ref for ===
(state) => state.otherValue,
(filtered, other) => {
return { other, filtered };
}
); becomes this: // filterOnly is memoized on immutableCollection, so doesn't change when
// immutableCollection doesn't change.
const filterOnly = createSelector(
(state) => state.immutableCollection // ONLY SOMETIMES gives new object ref for ===
(coll) => coll.filter((x) => x > 5),
);
const filterAndOther = createSelector(
filterOnly,
(state) => state.otherValue,
(filtered, other) => {
return { other, filtered };
}
); and the problem is solved. If you're still worried about performance for those rare times you need to use filter(), map(), etc in your selectors... memoize that small part of your selectors. |
For an example using a cool multi-item cache of previous selector inputs and only filtering in the selector, check out https://jsbin.com/kohonu/231/edit?js,console,output |
@cormacrelf heres another question, would you do your filter and or sort inside the reducer, or would you rather do it in your selectors with reselect? Why? |
Depends what you're doing. I'm about to do this in selectors to use this same pattern with normalized immutable.js lists -> filters |
You can initialize reselect with a different comparison function using createSelectorCreator. See the docs, the code, and most of the rest of this thread. I don't know what you're talking about on that one.
The whole tree doesn't re-render if only a middle-man piece of data changes. If you modify one part of the tree, yes, the new === equality propagates up to the root, but that's the point, it means Redux doesn't shoot itself. But it doesn't change unaffected parts. I have to use Angular now, with Edit: original comment from Domii was deleted. |
@cormacrelf Thank you for your clarification. And yeah, I deleted my comment, after I realized that I got a few things mixed up. Also thank you for I'll keep experimenting! :) |
No description provided.
The text was updated successfully, but these errors were encountered: