2016-11-22 39 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組件將重新呈現(以及那些訂閱它)。