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

Posted

技术标签:

【中文标题】在 React 中管理嵌套状态的好方法【英文标题】:Good approach to managing nested state in React 【发布时间】:2017-04-05 13:55:45 【问题描述】:

编辑:我重写了这个问题以澄清我所追求的 - 感谢到目前为止帮助我磨练它的人。

我试图了解如何最好地在 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...
  

在这个人为的示例中,状态有两个主要区域,可通过 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>
    );
  

...

假设AuthorList 有一堆Author 子组件,而PublicationList 有一堆Publication 子组件来渲染这些东西的实际内容。

这是我的问题:假设我想更新给定作者的bio,但我不希望为所有内容未更改的AuthorPublication 对象调用render() .

从这个答案:

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

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

人们如何处理像上面这样的复杂状态 - 在每次状态更改时对大量组件调用 render() 似乎不是一个好的解决方案(即使生成的渲染对象相同,因此不会发生更改到实际的 DOM)。

【问题讨论】:

【参考方案1】:

这是一种使用 Object Spread Syntax 高效且易读的方法。

let state = 
    authors : 
        ...this.state.authors, 
        [ givenId ] :  
            ...this.state.authors[ givenID ], 
            bio : newValue 
        
      

this.setState(state)

请记住,当您在 jsx 中映射项目时,您必须传递一个“密钥”作为道具。

这主要是因为,react 的协调(React 的“差异”算法来检查已更改的内容)会检查映射 jsx 的键(大致命名为 jsx)。

无论如何,在 reacts state/setState 或 redux 中管理状态与“和解”无关。

在这两种情况下,您都可以使用“对象传播语法”语法更改嵌套数据的一部分。

剩下的就是将“相同”的键传递给映射的 jsx。因此,尽管 react 会重新渲染,但它不会尝试对不必要的部分进行 dom 更新,这很昂贵。

【讨论】:

这不会重新渲染依赖于该州任何地方的所有内容吗?我确实很欣赏用于进行更新的更好的语法 - 但如果我每次更改一位作者的简历时都必须重新渲染每个组件,那似乎我做错了什么。 我发现这个答案有助于理解正在发生的事情:***.com/questions/24718709/… - 默认情况下,组件的 render() 函数将在其状态或其任何父项的状态发生变化时被调用- 无论该状态更改是否与该组件的道具有关。可以使用 shouldComponentUpdate 更改此行为。 是的,我知道虚拟 DOM 以及除非底层组件不同,否则 react 不会改变 DOM。尽管如此,如果我有 50 位作者和 100 篇出版物,我不想在只有少数组件的内容发生变化时运行 render() 150 次。【参考方案2】:

您应该使用immutability helper,每个React's documentation。这提供了一种更新部分状态的机制,并为您处理任何所需的克隆,尽可能减少重复。

这允许您执行以下操作:

this.setState(update(this.state,  authors:  $set:  ...   ));

它只会重新渲染受更改影响的组件。

【讨论】:

在这种情况下,您不是在创建一个新对象并用它覆盖整个状态吗?这不会重新渲染依赖于状态中任何内容的每个对象 - 即使实际更改被隔离到 this.state.authors['342'].bio 中? 我不知道它在内部是如何工作的,但根据我对 React 文档的阅读,它尽可能使用浅拷贝(并且我假设重复使用未更改的引用)。 我发现这个答案有助于理解正在发生的事情:***.com/questions/24718709/… - 默认情况下,组件的 render() 函数将在其状态或其任何父项的状态发生变化时被调用- 无论该状态更改是否与该组件的道具有关。可以使用 shouldComponentUpdate 更改此行为。 我认为 React 比这更聪明,但我想不是。 facebook.github.io/react/docs/optimizing-performance.html 和 blog.javascripting.com/2015/03/31/… 有一些关于覆盖 shouldComponentUpdate 以手动优化的信息。【参考方案3】:

我认为使用Redux 将使您的应用更高效且更易于管理。

具有称为 Redux 存储 的全局状态,它允许您的任何组件订阅存储的切片并在这些数据发生更改时重新呈现。

在您的示例中,redux 实现它的方式是您的AuthorList 组件将订阅state.authors 对象,如果AuthorList 组件内部或外部的任何组件更新state.authors,则只有@987654326 @ 组件将重新渲染(以及订阅它的那些)。

【讨论】:

【参考方案4】:

您的组件状态应该只真正包含内部状态值。

您应该考虑使用Redux 在多个组件中存储更复杂的状态。

【讨论】:

【参考方案5】:

感谢 jpdeatorre 和 daveols 指点我 Redux。

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

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

这是因为 Redux 为其连接的 react 组件提供了 shouldComponentUpdate 来评估 props 是否以及相关状态是否已更改。

请注意,这里 Redux 的优化很浅。要确定是否要跳过 render() Redux 的 shouldComponentUpdate 检查是否:

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

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

还要注意,为了防止在 Author 的“哑”组件上调用 render(),我必须将它 connect() 到 Redux 以启用 Redux 的 shouldComponentUpdate 逻辑 - 即使该组件根本什么都不做与 state 并只是读取它的 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' )
);

【讨论】:

嗯...不知道为什么你会回答你的问题,这实际上和我说的一样。 因为这两个答案实际上并不相同。每当 state.authors 中任何内容的状态发生变化时,您的结果都会重新呈现 AuthorList 的所有 Author 子项。我的有选择地仅重新渲染 AuthorList 的已更改内容的子项,这是我试图做的。

以上是关于在 React 中管理嵌套状态的好方法的主要内容,如果未能解决你的问题,请参考以下文章

React项目中使用Mobx状态管理

Redux / MobX - 我是否需要通过React中的props传递子组件中的数据?

在 React 上下文中更新嵌套状态的最佳方法

react监听仓库数据变化

如何使用 React Hooks 实现复杂组件的状态管理

用 setState() 更新嵌套的 React 状态属性的最佳选择是啥?