deadcode deadcode - 15 days ago 3
React JSX Question

Good approach to managing nested state in React

EDIT: I rewrote this question to clarify what I'm after - thank you to the people who have responded so far helping me hone it.

I am trying to understand how best to manage complex, nested state in React, while also limiting the number of times

render()
is called for components whose content has not changed.

As background:

Suppose I have state with both "authors" and "publications" in an object like this:

{
'authors' : {
234 : {
'name' : 'Alice Ames',
'bio' : 'Alice is the author of over ...',
'profile_pic' : 'http://....'
},
794 : {
'name' : 'Bob Blake',
'bio' : 'Hailing from parts unknown, Bob...',
'profile_pic' : 'http://....'
},
...more authors...
},
'publications' : {
539 : {
'title' : 'Short Story Vol. 2',
'author_ids' : [ 234, 999, 220 ]
},
93 : {
'title' : 'Mastering Fly Fishing',
'author_ids' : [ 234 ]
},
...more publications...
}
}


In this contrived example the state has two main areas, accessed by the
authors
and
publications
keys.

The
authors
key leads to an object keyed on an ID of the author, which leads to an object with some author data.

The
publications
key leads to an object keyed on the ID of the publication that has some publication data, and an array of authors.

Suppose my state is in an
App
component with child components like the following pseudo JSX:

...
<App>
<AuthorList authors={this.state.authors} />
<PublicationList authors={this.state.authors} publications={this.state.publications} />
</App>
...

...
class AuthorList extends React.Component {
render() {
let authors = this.props.authors;
return (
<div>
{ Object.keys( authors ).map( ( author_id ) => {
return <Author author={authors[author_id]} />;
}
</div>
);
}
}
...

...
class PublicationList extends React.Component {
render() {
let publications = this.props.publications;
let authors = this.props.authors;
return (
<div>
{ Object.keys( publications ).map( ( publication_id ) => {
return <Publication publication={publications[publication_id]} authors=authors />;
}
</div>
);
}
}
...


Assume
AuthorList
has a bunch of child
Author
components, and
PublicationList
has a bunch of child
Publication
components that render the actual content of those things.

Here is my question: Suppose I want to update the
bio
for a given author, but I don't want
render()
to be called for all the
Author
and
Publication
objects whose content have not changed.

From this answer:

ReactJS - Does render get called any time "setState" is called?

A React component's
render()
function will get called any time its state, or the state of any of its parents, change - regardless of whether that state change has anything to do with the props of that component. This behavior can be changed with shouldComponentUpdate.

How people handle complex state like the above - it doesn't seem like calling render() on large numbers of components on every state change is a good solution (even if the resulting rendered object is the same and so no change occur to the actual DOM).

Answer

Thank you to jpdeatorre and daveols for pointing me at Redux.

Here is an example application (with tons of corner cutting, but it shows the technique) of using Redux to isolate components from state changes that are irrelevant to them.

In this example the changes to the author Alice with id 1 don't cause Author components that don't depend on Alice to have their render() called.

This is because Redux's supplied shouldComponentUpdate for its connected react components evaluates whether the props and if relevant state have changed.

Be forewarned that Redux's optimization here is shallow. To determine wither or not to skip render() Redux's shouldComponentUpdate checks if:

  • The old and new props are === to one another
  • Or, if not that they have the same keys and the values of those keys are === to one another.

So it could result in render() being called for components whose values are still logically equivalent however the props to those components and their first level keys do not compare as equal with ===. See: https://github.com/reactjs/react-redux/blob/master/src/utils/shallowEqual.js

Note also that in order to prevent calling render() on the "dumb" component of Author I had to connect() it to Redux to enable Redux's shouldComponentUpdate logic - even though that component is doing nothing at all with the state and just reads its props.

import ReactDOM from 'react-dom';
import React from 'react';

import { Provider, connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';

import update from 'immutability-helper';


const updateAuthor = ( author ) => {
  return ( {
    type : 'UPDATE_AUTHOR',
    // Presently we always update alice and not a particular author, so this is ignored.
    author
  } );
};

const updateUnused = () => {
  return ( {
    type : 'UPDATE_UNUSUED',
    date : Date()
  } );
};

const initialState = {
  'authors': {
    1: {
      'name': 'Alice Author',
      'bio': 'Alice initial bio.'
    },
    2: {
      'name': 'Bob Baker',
      'bio': 'Bob initial bio.'
    }
  },
  'publications': {
    1 : {
      'title' : 'Two Authors',
      'authors' : [ 1, 2 ]
    },
    2 : {
      'title' : 'One Author',
      'authors' : [ 1 ]
    }
  }
};

const initialDate = Date();

const reduceUnused = ( state=initialDate, action ) => {
  switch ( action.type ) {
    case 'UPDATE_UNUSED':
      return action.date;

    default:
      return state;
  }
};

const reduceAuthors = ( state=initialState, action ) => {
  switch ( action.type ) {
    case 'UPDATE_AUTHOR':
      let new_bio = state.authors['1'].bio + ' updated ';
      let new_state = update( state, { 'authors' : { '1' : { 'bio' : {$set : new_bio } } } } );
      /*
      let new_state = {
        ...state,
        authors : {
          ...state.authors,
          [ 1 ] : {
            ...state.authors[1],
            bio : new_bio
          }
        }
      };
      */
      return new_state;

    default:
      return state;
  }
};

const testReducers = combineReducers( {
  reduceAuthors,
  reduceUnused
} );

const mapStateToPropsAL = ( state ) => {
  return ( {
    authors : state.reduceAuthors.authors
  } );
};

class AuthorList extends React.Component {

  render() {
    return (
      <div>
        { Object.keys( this.props.authors ).map( ( author_id ) => {
          return <Author key={author_id} author_id={author_id} />;
        } ) }
      </div>
    );
  }
}
AuthorList = connect( mapStateToPropsAL )(AuthorList);

const mapStateToPropsA = ( state, ownProps ) => {
  return ( {
    author : state.reduceAuthors.authors[ownProps.author_id]
  } );
};

class Author extends React.Component {

  render() {
    if ( this.props.author.name === 'Bob Baker' ) {
      alert( "Rendering Bob!" );
    }

    return (
      <div>
        <p>Name: {this.props.author.name}</p>
        <p>Bio: {this.props.author.bio}</p>
      </div>
    );
  }
}
Author = connect( mapStateToPropsA )( Author );


const mapStateToPropsPL = ( state ) => {
  return ( {
    authors : state.reduceAuthors.authors,
    publications : state.reduceAuthors.publications
  } );
};


class PublicationList extends React.Component {

  render() {
    console.log( 'Rendering PublicationList' );
    let authors = this.props.authors;
    let publications = this.props.publications;
    return (
      <div>
        { Object.keys( publications ).map( ( publication_id ) => {
          return <Publication key={publication_id} publication={publications[publication_id]} authors={authors} />;
        } ) }
      </div>
    );
  }
}
PublicationList = connect( mapStateToPropsPL )( PublicationList );


class Publication extends React.Component {

  render() {
    console.log( 'Rendering Publication' );
    let authors = this.props.authors;
    let publication_authors = this.props.publication.authors.reduce( function( obj, x ) {
      obj[x] = authors[x];
      return obj;
    }, {} );

    return (
      <div>
        <p>Title: {this.props.publication.title}</p>
        <div>Authors:
          <AuthorList authors={publication_authors} />
        </div>
      </div>
    );
  }
}

const mapDispatchToProps = ( dispatch ) => {
  return ( {
    changeAlice : ( author ) => {
      dispatch( updateAuthor( author ) );
    },
    changeUnused : () => {
      dispatch( updateUnused() );
    }
  } );
};

class TestApp extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <p>
          <span onClick={ () => { this.props.changeAlice( this.props.authors['1'] ); } }><b>Click to Change Alice!</b></span>
        </p>
        <p>
          <span onClick={ () => { this.props.changeUnused(); } }><b>Click to Irrelevant State!</b></span>
        </p>

        <div>Authors:
          <AuthorList authors={this.props.authors} />
        </div>
        <div>Publications:
          <PublicationList authors={this.props.authors} publications={this.props.publications} />
        </div>
      </div>
    );
  }
}
TestApp = connect( mapStateToPropsAL, mapDispatchToProps )( TestApp );

let store = createStore( testReducers );

ReactDOM.render(
  <Provider store={store}>
    <TestApp />
  </Provider>,
  document.getElementById( 'test' )
);