Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to optimize small updates to props of nested component in React + Redux?

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).

enter image description here

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 

enter image description here

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 
like image 709
Daiwei Avatar asked May 16 '16 22:05

Daiwei


People also ask

Does Redux solve prop drilling?

Both Redux and React's Context API deal with "prop drilling". That said, they both allow you to pass data without having to pass the props through multiple layers of components.

Should I keep all component's state in Redux store?

Some users prefer to keep every single piece of data in Redux, to maintain a fully serializable and controlled version of their application at all times. Others prefer to keep non-critical or UI state, such as “is this dropdown currently open”, inside a component's internal state. Using local component state is fine.

Is Redux better than Usecontext?

Redux is much more powerful and provides a set of handy features that Context doesn't have. It's great for managing centralized state and handling API requests.


1 Answers

I’m not sure where const App = connect((state) => state)(RepoList) comes from.
The corresponding example in React Redux docs has a notice:

Don’t do this! It kills any performance optimizations because TodoApp will rerender after every action. It’s better to have more granular connect() on several components in your view hierarchy that each only listen to a relevant slice of the state.

We don’t suggest using this pattern. Rather, each connect <Repo> specifically so it reads its own data in its mapStateToProps. 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 from reposById, and then only have your RepoList re-render if repoIds change. This way changes to individual repos won’t affect the list itself, and only the corresponding Repo 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 to connect():

const makeMapStateToProps = (initialState, initialOwnProps) => {   const { id } = initialOwnProps   const mapStateToProps = (state) => {     const { todos } = state     const todo = todos.byId[id]     return {       todo     }   }   return mapStateToProps }  export default connect(   makeMapStateToProps )(TodoItem) 

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 using initialOwnProps does not incur this penalty because it is only used once.

A fast version of your example would look like this:

import React from 'react'; import ReactDOM from 'react-dom'; import {createStore} from 'redux'; import {Provider, connect} from 'react-redux'; import set from 'lodash/fp/set'; import pipe from 'lodash/fp/pipe'; import groupBy from 'lodash/fp/groupBy'; import mapValues from 'lodash/fp/mapValues';  const UPDATE_TAG = 'UPDATE_TAG';  const reposById = pipe(   groupBy('id'),   mapValues(repos => repos[0]) )(require('json!../repos.json'));  const repoIds = Object.keys(reposById);  const store = createStore((state = {repoIds, reposById}, action) => {   switch (action.type) {   case UPDATE_TAG:     return set('reposById.1.tags[0]', {id: 213, text: 'Node.js'}, state);   default:     return state;   } });  const Repo  = ({repo}) => {   const [authorName, repoName] = repo.full_name.split('/');   return (     <li className="repo-item">       <div className="repo-full-name">         <span className="repo-name">{repoName}</span>         <span className="repo-author-name"> / {authorName}</span>       </div>       <ol className="repo-tags">         {repo.tags.map((tag) => <li className="repo-tag-item" key={tag.id}>{tag.text}</li>)}       </ol>       <div className="repo-desc">{repo.description}</div>     </li>   ); }  const ConnectedRepo = connect(   (initialState, initialOwnProps) => (state) => ({     repo: state.reposById[initialOwnProps.repoId]   }) )(Repo);  const RepoList = ({repoIds}) => {   return <ol className="repos">{repoIds.map((id) => <ConnectedRepo repoId={id} key={id}/>)}</ol>; };  const App = connect(   (state) => ({repoIds: state.repoIds}) )(RepoList);  console.time('INITIAL'); ReactDOM.render(   <Provider store={store}>     <App/>   </Provider>,   document.getElementById('app') ); console.timeEnd('INITIAL');  setTimeout(() => {   console.time('DISPATCH');   store.dispatch({     type: UPDATE_TAG   });   console.timeEnd('DISPATCH'); }, 1000); 

Note that I changed connect() in ConnectedRepo to use a factory with initialOwnProps rather than ownProps. 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 in connect().

This approach beats both previous approaches in my testing:

one-connect.js: 43.272ms repo-connect.js before changes: 61.781ms repo-connect.js after changes: 19.954ms 

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.


I got a solution by replacing every tags with an observable inside reducer.

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.

like image 127
Dan Abramov Avatar answered Sep 28 '22 10:09

Dan Abramov