Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork3.3k
Bail out if stateProps can be calculated early and did not change#348
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
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 againsthttps://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 functionmakeMapStateToProps(initialState,initialOwnProps){varid=initialOwnProps.idreturnfunctionmapStateToProps(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 |
ghost commentedApr 13, 2016
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(letreducerKeyofreducerKeys){this.unsubscribe.push(store.watch(// when some reducer returns a new statereducerKey,nextState=>{// update wrapped component's propsthis.componentProps[reducerKey]=nextState;this.doUpdate=true;// and we know for sure we should update}));}this.unsubscribe.push(store.subscribe(()=>{// after action has completedif(this.doUpdate){// simply check for doUpdate flagthis.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 listconstmerge={item:{// this is run only for components with an `item` propTypekeys:['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 changedconst{ list}=state;const{ itemIndex}=props;returnlist[itemIndex]||null;}}}; There are alreadytests 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 <Todoid={123}className="someName"/> to <Todoid={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) |
joonhyublee commentedApr 25, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@gaearon Here ismy use case, in which early bailout yields significant performance boost. With regards to@slorber's concern that propscan 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 aboveconstmakeMapStateToProps=(initialState,initialProps)=>{const{ userID}=initialProps;//props that's not expected to change (often)constmapStateToProps=(state)=>{constuserNickname=state.users[userID].nickname;//slice of state dependent on propsconstuserAvatar=state.users[userID].avatar;//slice of state dependent on propsreturn{ userNickname, userAvatar};};returnmapStateToProps;}constUserComponent=React.createClass({// component implementation});exportdefaultconnect(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: Icould revert back to //instead of this,exportdefaultconnect(makeMapStateToProps)(UserComponent);//do this:constConnectedComponent=connect(makeMapStateToProps)(UserComponent);exportdefault(props)=>(<ConnectedComponentkey={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: exportdefault(props)=>(<ConnectedComponentkey={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 shouldfixreduxjs/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 thatdo care about
ownPropsdue to#99.Above, I said:
However in many cases it’s possible to convert a component that needs
ownPropsto a component that doesn’t.How? In#279, we added the ability to specify factories instead of
mapStateToPropsandmapDispatchToProps. Curiously, these factoriesthemselves do receivestateandownPropsas arguments. Of course, they are only invoked once,but if you’re only usingownPropsto read an ID (which is a common case in large lists) and use stablekeys, you should be able to changeinto
See? Obviously
idwould never get updated, but we don’t need it to (in this particular case). And we can always addownPropslater (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
mapStateToPropsthat doesn’t depend onownProps.