编辑:我重写了这个问题以澄清我之后的事情 - 感谢那些迄今为止帮助我磨练它的人。
我试图了解如何最好地在React中管理复杂的嵌套状态,同时还限制为内容未发生变化的组件调用render()
的次数。
作为背景:
假设我和'#34;作者"和"出版物"在这样的对象中:
{
'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...
}
}
在这个人为的例子中,州有两个主要区域,可以通过authors
和publications
键访问。
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()
和{Author
调用Publication
{1}}内容未更改的对象。
从这个回答:
ReactJS - Does render get called any time "setState" is called?
React组件的render()
函数将在其状态或其任何父项的状态发生变化时被调用 - 无论该状态更改是否与其中的道具有关。零件。可以使用shouldComponentUpdate更改此行为。
人们如何处理如上所述的复杂状态 - 在每次状态变化时对大量组件调用render()似乎都是一个很好的解决方案(即使生成的渲染对象是相同的,所以没有改变发生在实际的DOM)。
答案 0 :(得分:5)
这是一种使用对象传播语法以高效可读的方式实现此目的的方法。
let state = {
authors : {
...this.state.authors,
[ givenId ] : {
...this.state.authors[ givenID ],
bio : newValue
}
}
}
this.setState(state)
请记住,你必须传递一个键。在jsx中映射项目时作为道具。
这主要是因为,协调(React&#34;差异&#34;算法来检查已发生的变化)反应的事情会检查映射jsx的密钥(大致命名为jsx)。
无论如何,在状态/ setState或者redux中管理状态与&#39;对帐&#39;无关。
在这两种情况下,您都可以使用&#39;对象传播语法&#39;来更改嵌套数据的一部分。语法。
你要关心的其余部分是通过相同的&#39;映射的jsx的键。因此,尽管反应重新出现,但它并没有尝试对不必要的部分进行dom更新,这是昂贵的。
答案 1 :(得分:3)
您应该使用immutability helper,每React's documentation。这提供了一种更新部分状态的机制,并为您处理任何所需的克隆,并尽可能少地复制。
这允许您执行以下操作:
this.setState(update(this.state, { authors: { $set: { ... } } }));
它只会重新渲染受更改影响的组件。
答案 2 :(得分:2)
我认为使用Redux可以让您的应用更高效,更易于管理。
拥有一个称为 Redux store 的全局状态,它允许您的任何组件订阅商店的一部分,并在这些数据发生更改时重新呈现。
在您的示例中,实现它的redux方式是您的AuthorList
组件将订阅state.authors
对象,如果AuthorList
组件内部或外部的任何组件更新了{{ 1}},只有state.authors
组件会重新渲染(以及那些订阅它的组件)。
答案 3 :(得分:0)
您的组件状态应该只包含内部状态值。
您应该考虑使用Redux存储多个组件中所需的更复杂状态。
答案 4 :(得分:-3)
感谢jpdeatorre和daveols指点我Redux。
这是一个示例应用程序(有大量的角切割,但它显示了技术),使用Redux将组件与状态变化隔离开来。
在此示例中,对ID为1的作者Alice的更改不会导致Author
组件不依赖于Alice调用其render()。
这是因为Redux为其连接的反应组件提供shouldComponentUpdate
来评估道具和相关状态是否已经改变。
预先警告Redux的优化很浅。要确定是否要跳过render()
Redux的shouldComponentUpdate
检查是否:
===
===
。因此,它可能导致render()
被调用其值仍然在逻辑上相等的组件,但是这些组件的道具和它们的第一级键不会与===
相等。请参阅:https://github.com/reactjs/react-redux/blob/master/src/utils/shallowEqual.js
另请注意,为了防止在&#34; dumb&#34;上调用render()
Author
的组件我必须connect()
它才能启用Redux的shouldComponentUpdate
逻辑 - 即使该组件对状态一无所知,只是读取它的道具。
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' )
);