2016-11-22 59 views
8

编辑:我重写了这个问题,以澄清我后 - 谢谢你的回应迄今为止帮助我磨练它的人。在React中管理嵌套状态的好方法

我想了解如何在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对于给定的作家,但我不希望render()被称为所有AuthorPublication对象,其内容并没有改变。

从这样的回答:

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

一个阵营组件的render()功能将被调用任何时候的状态,或任何其母公司,变化的状态 - 无论该状态变化是否有什么使用该组件的道具。这个行为可以用shouldComponentUpdate来改变。

人们如何处理像上面这样复杂的状态 - 它看起来好像在每个状态上调用大量组件的render()方法都是一个很好的解决方案(即使生成的渲染对象是相同的并且没有变化发生在实际的DOM上)。

回答

-2

谢谢jpdeatorre和daveols指点我在Redux。

下面是使用Redux将组件与状态变化隔离的示例应用程序(包含大量切角,但它显示了该技术),这些变化与它们无关。

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

这是因为Redux提供的shouldComponentUpdate为其连接的反应组件评估道具和相关状态是否发生了变化。

预先警告Redux在这里的优化很浅。为了确定枯萎或不跳过render()终极版的shouldComponentUpdate检查:

  • 旧的和新的道具是===彼此
  • 或者,如果不是他们具有相同的键和这些键的值是===彼此。

所以它可能会导致render()正在呼吁其值仍是逻辑上等同但是道具的组件和他们的第一级密钥不比较===的分量相等。请参阅:https://github.com/reactjs/react-redux/blob/master/src/utils/shallowEqual.js

还要注意的是,为了防止Author我不得不connect()它终极版,使终极版的shouldComponentUpdate逻辑“哑巴”组件上调用render() - 尽管该组件是做什么都与国家和只是读它的道具。

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') 
); 
+1

嗯...不知道为什么你会回答你的问题,这与我所说的几乎相同。 – jpdelatorre

+0

因为这两个答案实际上并不相同。您的结果是每当State.authors中的任何状态发生更改时,AuthorList的所有Author子级都会重新呈现。我有选择地重新渲染只改变了内容的AuthorList的孩子,这正是我想要做的。 – deadcode

0

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

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

2

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

这允许你做这样的事情:

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

它只会重新渲染了由变化影响的组件。

+0

在这种情况下你不是在创建一个新的对象并用它覆盖整个状态吗?这不会重新呈现依赖于状态中任何事物的每一个对象 - 即使在this.state.authors ['342'] .blood中实际的变化被隔离了, – deadcode

+0

我不知道它是如何在内部工作的,但是基于我对React文档的阅读,它尽可能使用浅拷贝(并且我假定重新使用不变的引用)。 –

+0

我发现这个答案有助于理解发生了什么事情:http://stackoverflow.com/questions/24718709/reactjs-does-render-get-called-any-time-setstate-is-called - 默认情况下组件的呈现()函数在其状态或任何父辈的状态发生变化时都会被调用 - 无论该状态变化是否与该组件的道具有关。这个行为可以用shouldComponentUpdate来改变。 – deadcode

4

这是一种使用对象扩展语法高效完成此操作的方法。

let state = { 
    authors : { 
     ...this.state.authors, 
     [ givenId ] : { 
      ...this.state.authors[ givenID ], 
      bio : newValue 
     } 
    } 
} 
this.setState(state) 

请记住,当您在jsx中映射项目时,您必须传递'key'作为道具。

这主要是因为,调整(React的“差异”算法来检查发生了什么变化)的事情,确实检查映射的jsx的键(大致命名为jsx)。

无论如何,管理state in state/setState或者redux与'reconciliation'无关。

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

所有你会关心的其余问题是将'相同'的键传递给映射的jsx。因此,虽然反应放弃,但它不会尝试对不必要的部分进行dom更新,这很昂贵。

+0

这不是重新渲染一切取决于状态中任何地方的任何东西吗?我很欣赏更新的更好语法 - 但是如果每次更改一个作者的生物时,我必须重新渲染每个组件,那么看起来我似乎做错了什么。 – deadcode

+0

你认为通过引用比较newState和oldState的反应?这是不正确的,对newState和oldState的键进行深入比较。由于只有特定的键值发生了变化,它才会重新渲染组件中的相应部分。 – FurkanO

+0

我发现这个答案有助于理解发生了什么事情:http://stackoverflow.com/questions/24718709/reactjs-does-render-get-called-any-time-setstate-is-called - 默认情况下组件的呈现()函数在其状态或任何父辈的状态发生变化时都会被调用 - 无论该状态变化是否与该组件的道具有关。这个行为可以用shouldComponentUpdate来改变。 – deadcode

1

我认为使用Redux可以让您的应用更加高效和易于管理。

具有全局状态被称为Redux的商店,它可以让你的任何组件的订阅到店里一片,当有改动的数据重新呈现。

在你的榜样,实现它的终极版方式将是你AuthorList组件将订阅state.authors对象,如果任何部件内或AuthorList组件更新state.authors外,只有AuthorList组件将重新呈现(以及那些订阅它)。