-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Bail out if stateProps can be calculated early and did not change #348
Conversation
cc @slorber, @ellbee, @tgriesser who might be interested in this |
This one looks like a clear win to me, but I wonder if it would be worth working on a benchmark suite again like the one started in #104? I've used benchmark.js before, but not for testing UI code. Is it even really feasible? |
For now, I’m testing against https://github.com/mweststrate/redux-todomvc manually. |
Yeah, I have just been reading through that issue. It is what made me start thinking about it. I might have a play around with it. |
👍 |
Oh wait, I’m supposed to emoji your post inline. |
@gaearon this is great news that connect not using component's props get faster :) However I'm not sure the current implementation is the best we could do:
Instead of function makeMapStateToProps(initialState, initialOwnProps) {
var id = initialOwnProps.id
return function mapStateToProps(state) {
return {
item: state.items[id]
}
}
}
connect(makeMapStateToProps)(Component) I would rather have something like: connect({
mapperPropsSelector: ownProps => {id: ownProps.id},
mapper: (state,{id}) => state.items[id]
})(Component) (this is probably not the best API but you get the idea) What it permits here is to make it clear on what the mapping function has to rely on to do its job, so you can more easily detect when props needed for the mapping have changed and run the mapper only when needed And I think, by default, not providing any ownProps to the mapping function (ie no selector provided) would actually encourage people to write more optimized code by default (unlike factories which would be opt-in optimization), but it would be a breaking change (This is related to my proposal for more flexible subscriptions here: #269 (comment)) |
I’m confused: how does accepting a selector for props solve the problem of |
Sorry if I'm going on a tangent with this comment but I'll share some performance optimizations I implemented earlier this month when I rewrote I added a Condensed version: for (let reducerKey of reducerKeys) {
this.unsubscribe.push(
store.watch( // when some reducer returns a new state
reducerKey, nextState => { // update wrapped component's props
this.componentProps[reducerKey] = nextState;
this.doUpdate = true; // and we know for sure we should update
}
)
);
}
this.unsubscribe.push(
store.subscribe(() => { // after action has completed
if (this.doUpdate) { // simply check for doUpdate flag
this.forceUpdate();
}
})
); See the full version where the same method is used for props derived from some combination of state and own props: To elaborate a little bit more, deriving props from a combination of state and own props looks like this: // here we'll provide some item from some list
const merge = {
item: { // this is run only for components with an `item` propType
keys: ['list'], // the provided `item` depends on the state of the `list`
get(state, props, context) {
// this function is executed only when `list` changes
// and components will be updated only when the `item` has changed
const { list } = state;
const { itemIndex } = props;
return list[itemIndex] || null;
}
}
}; There are already tests to ensure renders occur only as absolutely necessary, but more in-depth performance tests are coincidentally next on my todo list. I was planning on seeing how things perform when incorporated into |
@gaearon sorry it's hard to think well on such code late in the night :) My proposal may not prevent the The problem I see with current code is that for example if this component changes from <Todo id={123} className="someName"/> to <Todo id={123} className="anotherClassName"/> then yes we necessarily have to do a This line: shouldUpdateStateProps = hasStoreStateChanged || (
haveOwnPropsChanged && this.doStatePropsDependOnOwnProps
) it will make Also, not a big deal, but not sure This is only what I can say for now, I have to study the code a bit more to say anything else :) (ie #99) |
@gaearon Here is my use case, in which early bailout yields significant performance boost. With regards to @slorber's concern that props can change, I encountered a similar issue, and I did this hack to get around it. Let's say we have: //factory for map state to props, as per Dan's suggestion above
const makeMapStateToProps = (initialState, initialProps) => {
const { userID } = initialProps; //props that's not expected to change (often)
const mapStateToProps = (state) => {
const userNickname = state.users[userID].nickname; //slice of state dependent on props
const userAvatar = state.users[userID].avatar; //slice of state dependent on props
return {
userNickname,
userAvatar
};
};
return mapStateToProps;
}
const UserComponent = React.createClass({
// component implementation
});
export default connect(makeMapStateToProps)(UserComponent); Of course, userID isn't expected to change, but there may be some cases where it does. For example, the above component may be a react-router route component, which gets its userID from the pathname: I could revert back to //instead of this,
export default connect(makeMapStateToProps)(UserComponent);
//do this:
const ConnectedComponent = connect(makeMapStateToProps)(UserComponent);
export default (props) => (
<ConnectedComponent key={ props.userID } {...props}/>
); Because of key, when userID is changed, the old component is completely unmounted, and new one is mounted instead, with the updated props. It's a bit hacky, but I think this is an acceptable trade-off. I guess for multiple props that aren't expected to change often I can stringify as a key: export default (props) => (
<ConnectedComponent key={ props.userID + '_' + props.anotherSparselyChangedProp }/>
); but that .. is beyond ugly. It does bother me there are so many different steps within different levels involved in simply invalidating and updating a component, but was the only way I could make this work. |
nice trick @joonhyublee :D Maybe this usecase will be easier to handle once the code of connect becomes much simpler, and it might with #368 |
This should fix reduxjs/redux#1437 and #300.
If the component doesn’t care about
ownProps
, we can bail out of callingsetState()
.This PR implements this optimization.
As a reminder, we can’t do this for components that do care about
ownProps
due to #99.Above, I said:
However in many cases it’s possible to convert a component that needs
ownProps
to a component that doesn’t.How? In #279, we added the ability to specify factories instead of
mapStateToProps
andmapDispatchToProps
. Curiously, these factories themselves do receivestate
andownProps
as arguments. Of course, they are only invoked once, but if you’re only usingownProps
to read an ID (which is a common case in large lists) and use stablekey
s, you should be able to changeinto
See? Obviously
id
would never get updated, but we don’t need it to (in this particular case). And we can always addownProps
later (of course at the performance cost).I think it’s a neat trick, and it will benefit from merging this optimization, since this optimization is relevant to any
mapStateToProps
that doesn’t depend onownProps
.