在React中管理嵌套状态的好方法

dea*_*ode 9 reactjs

编辑:我重写了这个问题,以澄清我所追求的内容 - 感谢迄今为止帮助我磨练它的人们.

我试图了解如何最好地在React中管理复杂的嵌套状态,同时还限制render()内容未更改的组件的调用次数.

作为背景:

假设我在这样的对象中有"作者"和"出版物"的状态:

{
  '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...
  }
}
Run Code Online (Sandbox Code Playgroud)

在这个人为的例子中,州有两个主要区域,由authorspublications键访问.

authors键导致键连接在作者,这导致对象与一些作者的数据的ID的对象.

publications键导致的对象键连接在具有一些出版物数据的公布,和作者的阵列的ID.

假设我的状态在一个App包含子组件的组件中,如下面的伪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>
    );
  }
}
...
Run Code Online (Sandbox Code Playgroud)

假设AuthorList有一堆子Author组件,并且PublicationList有一堆子Publication组件可以呈现这些组件的实际内容.

这里是我的问题:假设我想更新bio对于给定的作家,但我不希望render()被称为所有AuthorPublication对象,它们的内容都没有改变.

从这个答案:

ReactJS - 在调用"setState"的任何时候都会调用render吗?

React组件的render()函数将在其状态或其任何父级的状态发生变化时被调用 - 无论该状态更改是否与该组件的props有关.可以使用shouldComponentUpdate更改此行为.

人们如何像上面那样处理复杂的状态 - 看起来似乎不是在每个状态变化上对大量组件调用render()都是一个很好的解决方案(即使生成的渲染对象是相同的,因此实际上不会发生变化DOM).

Fur*_*anO 6

这是一种使用对象传播语法以可读方式有效实现此目的的方法.

let state = {
    authors : {
        ...this.state.authors, 
        [ givenId ] : { 
            ...this.state.authors[ givenID ], 
            bio : newValue 
        }
    }  
}
this.setState(state)
Run Code Online (Sandbox Code Playgroud)

请记住,当您在jsx中映射项目时,必须将"键"作为道具传递.

这主要是因为,协调(React的"差异"算法来检查发生了什么变化)反应的事情会检查映射的jsx的密钥(大致命名为jsx).

无论如何,在state/stateState或redux中管理状态与"reconciliation"无关.

在这两种情况下,您都可以使用"Object Spread Syntax"语法更改嵌套数据的一部分.

您只关心其余的是将"相同"键传递给映射的jsx.因此,尽管反应重新出现,但它并没有尝试对不必要的部分进行dom更新,这是昂贵的.


dea*_*ode -3

感谢 jpdeatorre 和 daveols 向我指出 Redux。

这是一个使用 Redux 将组件与与其无关的状态更改隔离开来的示例应用程序(包含大量切角,但它展示了技术)。

在此示例中,对 id 1 的作者 Alice 的更改不会导致Author不依赖于 Alice 的组件调用其 render() 。

这是因为 ReduxshouldComponentUpdate为其连接的 React 组件提供了评估 props 以及相关状态是否已更改的信息。

预先警告一下,Redux 这里的优化是浅薄的。要确定是否枯萎,请跳过render()Redux 的shouldComponentUpdate检查,如果:

  • 新旧道具互为===一物
  • 或者,如果不是的话,它们具有相同的键,并且这些键的值===彼此相同。

因此,这可能会导致render()调用其值在逻辑上仍然等效的组件,但是这些组件的 props 及其第一级键与===. 请参阅: https: //github.com/reactjs/react-redux/blob/master/src/utils/shallowEqual.js

另请注意,为了防止调用render()“哑”组件,Author我必须使用connect()Redux 来启用 Redux 的shouldComponentUpdate逻辑 - 即使该组件对状态根本不执行任何操作,只是读取其 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' )
);
Run Code Online (Sandbox Code Playgroud)

  • 嗯...不知道为什么你会对你的问题给出与我所说的几乎相同的答案。 (2认同)