Example code: https://github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js
View live demo: http://d6u.github.io/example-redux-update-nested-props/one-connect.html
How to optimize small updates to props of nested component?
I have above components, Repo and RepoList. I want to update the tag of the first repo (Line 14). So I dispatched an UPDATE_TAG
action. Before I implemented shouldComponentUpdate
, the dispatch takes about 200ms, which is expected since we are wasting lots of time diffing <Repo/>
s that haven't changed.
After added shouldComponentUpdate
, dispatch takes about 30ms. After production build React.js, the updates only cost at about 17ms. This is much better, but timeline view in Chrome dev console still indicate jank frame (longer than than 16.6ms).
Imagine if we have many updates like this, or <Repo/>
is more complicated than current one, we won't be able to maintain 60fps.
My question is, for such small updates to a nested component's props, is there a more efficient and canonical way to update the content? Can I still use Redux?
I got a solution by replacing every tags
with an observable inside reducer. Something like
// inside reducer when handling UPDATE_TAG action
// repos[0].tags of state is already replaced with a Rx.BehaviorSubject
get('repos[0].tags', state).onNext([{
id: 213,
text: 'Node.js'
}]);
Then I subscribe to their values inside Repo component using https://github.com/jayphelps/react-observable-subscribe. This worked great. Every dispatch only costs 5ms even with development build of React.js. But I feel like this is an anti-pattern in Redux.
Update 1
I followed the recommendation in Dan Abramov's answer and normalized my state and updated connect components
The new state shape is:
{
repoIds: ['1', '2', '3', ...],
reposById: {
'1': {...},
'2': {...}
}
}
I added console.time
around ReactDOM.render
to time the initial rendering.
However, the performance is worse than before (both initial rendering and updating). (Source: https://github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js, Live demo: http://d6u.github.io/example-redux-update-nested-props/repo-connect.html)
// With dev build
INITIAL: 520.208ms
DISPATCH: 40.782ms
// With prod build
INITIAL: 138.872ms
DISPATCH: 23.054ms
I think connect on every <Repo/>
has lots of overhead.
Update 2
Based on Dan's updated answer, we have to return connect
's mapStateToProps
arguments return an function instead. You can check out Dan's answer. I also updated the demos.
Below, the performance is much better on my computer. And just for fun, I also added the side effect in reducer approach I talked (source, demo) (seriously don't use it, it's for experiment only).
// in prod build (not average, very small sample)
// one connect at root
INITIAL: 83.789ms
DISPATCH: 17.332ms
// connect at every <Repo/>
INITIAL: 126.557ms
DISPATCH: 22.573ms
// connect at every <Repo/> with memorization
INITIAL: 125.115ms
DISPATCH: 9.784ms
// observables + side effect in reducers (don't use!)
INITIAL: 163.923ms
DISPATCH: 4.383ms
Update 3
Just added react-virtualized example based on "connect at every with memorization"
INITIAL: 31.878ms
DISPATCH: 4.549ms
I’m not sure where
const App = connect((state) => state)(RepoList)
comes from.The corresponding example in React Redux docs has a notice:
We don’t suggest using this pattern. Rather, each connect
<Repo>
specifically so it reads its own data in itsmapStateToProps
. The “tree-view” example shows how to do it.If you make the state shape more normalized (right now it’s all nested), you can separate
repoIds
fromreposById
, and then only have yourRepoList
re-render ifrepoIds
change. This way changes to individual repos won’t affect the list itself, and only the correspondingRepo
will get re-rendered. This pull request might give you an idea of how that could work. The “real-world” example shows how you can write reducers that deal with normalized data.Note that in order to really benefit from the performance offered by normalizing the tree you need to do exactly like this pull request does and pass a
mapStateToProps()
factory toconnect()
:The reason this is important is because we know IDs never change. Using
ownProps
comes with a performance penalty: the inner props have to be recalculate any time the outer props change. However usinginitialOwnProps
does not incur this penalty because it is only used once.A fast version of your example would look like this:
Note that I changed
connect()
inConnectedRepo
to use a factory withinitialOwnProps
rather thanownProps
. This lets React Redux skip all the prop re-evaluation.I also removed the unnecessary
shouldComponentUpdate()
on the<Repo>
because React Redux takes care of implementing it inconnect()
.This approach beats both previous approaches in my testing:
Finally, if you need to display such a ton of data, it can’t fit in the screen anyway. In this case a better solution is to use a virtualized table so you can render thousands of rows without the performance overhead of actually displaying them.
If it has side effects, it’s not a Redux reducer. It may work, but I suggest to put code like this outside Redux to avoid confusion. Redux reducers must be pure functions, and they may not call
onNext
on subjects.