詳解React中傳入組件的props改變時(shí)更新組件的幾種實(shí)現(xiàn)方法
我們使用react的時(shí)候常常需要在一個(gè)組件傳入的props更新時(shí)重新渲染該組件,常用的方法是在componentWillReceiveProps中將新的props更新到組件的state中(這種state被成為派生狀態(tài)(Derived State)),從而實(shí)現(xiàn)重新渲染。React 16.3中還引入了一個(gè)新的鉤子函數(shù)getDerivedStateFromProps來專門實(shí)現(xiàn)這一需求。但無論是用componentWillReceiveProps還是getDerivedStateFromProps都不是那么優(yōu)雅,而且容易出錯(cuò)。所以今天來探討一下這類實(shí)現(xiàn)會(huì)產(chǎn)生的問題和更好的實(shí)現(xiàn)方案。
何時(shí)使用派生狀態(tài)
咱們先來看一個(gè)比較常見的需求,一個(gè)用戶列表,可以新增和編輯用戶,當(dāng)用戶點(diǎn)擊‘新建'
按鈕用戶可以在輸入框中輸入新的用戶名;當(dāng)點(diǎn)擊‘編輯'按鈕的時(shí)候,輸入框中顯示被編輯的用戶名,用戶可以修改;當(dāng)用戶點(diǎn)擊‘確定'按鈕的時(shí)候用戶列表更新。
class UserInput extends React.Component {
state = {
user: this.props.user
}
handleChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}
render() {
const { onConfirm } = this.props;
const { user } = this.state;
return (
<div>
<input value={user.name || ''} onChange={this.handleChange} />
<button onClick={() => { onConfirm(user) }}>確定</button>
</div>
);
}
}
class App extends React.Component {
state = {
users: [
{ id: 0, name: 'bruce' },
{ id: 1, name: 'frank' },
{ id: 2, name: 'tony' }
],
targetUser: {}
}
onConfirm = (user) => {
const { users } = this.state;
const target = users.find(u => u.id === user.id);
if (target) {
this.setState({
users: [
...users.slice(0, users.indexOf(target)),
user,
...users.slice(users.indexOf(target) + 1)
]
});
} else {
const id = Math.max(...(users.map(u => u.id))) + 1;
this.setState({
users: [
...users,
{
...user,
id
}
]
});
}
}
render() {
const { users, targetUser } = this.state;
return (
<div>
<UserInput user={targetUser} onConfirm={this.onConfirm} />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => { this.setState({ targetUser: u }) }}>編輯</button>
</li>
))
}
</ul>
<button onClick={() => { this.setState({ targetUser: {} }) }}>新建</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
運(yùn)行后,效果如圖:

現(xiàn)在點(diǎn)擊‘編輯'和‘新建'按鈕,輸入框中的文字并不會(huì)切換,因?yàn)辄c(diǎn)擊‘編輯'和‘更新'時(shí),雖然UserInput的props改變了但是并沒有觸發(fā)state的更新。所以需要實(shí)現(xiàn)props改變引發(fā)state更新,在UserInput中增加代碼:
componentWillReceiveProps(nextProps) {
this.setState({
user: nextProps.user
});
}
或者
static getDerivedStateFromProps(props, state) {
return {
user: props.user
};
}
這樣就實(shí)現(xiàn)了UserInput每次接收新的props的時(shí)候自動(dòng)更新state。但是這種實(shí)現(xiàn)方式是有問題的。
派生狀態(tài)導(dǎo)致的問題
首先來明確組件的兩個(gè)概念:受控?cái)?shù)據(jù)(controlled data lives)和不受控?cái)?shù)據(jù)(uncontrollered data lives)。受控?cái)?shù)據(jù)指的是組件中通過props傳入的數(shù)據(jù),受到父組件的影響;不受控?cái)?shù)據(jù)指的是完全由組件自己管理的狀態(tài),即內(nèi)部狀態(tài)(internal state)。而派生狀態(tài)揉合了兩種數(shù)據(jù)源,當(dāng)兩種數(shù)據(jù)源產(chǎn)生沖突時(shí),問題隨之產(chǎn)生。
問題一
當(dāng)在修改一個(gè)用戶的時(shí)候,點(diǎn)擊‘確定'按鈕,輸入框里的文字又變成了修改之前的文字。比如我將‘bruce'修改為‘bruce lee',確定后,輸入框中又變成了‘bruce',這是我們不愿意看到的。

出現(xiàn)這個(gè)問題的原因是,點(diǎn)擊確定,App會(huì)re-render,App又將之前的user作為props傳遞給了UserInput。我們當(dāng)然可以在每次點(diǎn)擊確定之后將targetUser重置為一個(gè)空對(duì)象,但是一旦狀態(tài)多了之后,這樣管理起來非常吃力。
問題二
假設(shè)頁面加載完成后,會(huì)異步請(qǐng)求一些數(shù)據(jù)然后更新頁面,如果用戶在請(qǐng)求完成頁面刷新之前已經(jīng)在輸入框中輸入了一些文字,隨著頁面的刷新輸入框中的文字會(huì)被清除。
我們可以在App中加入如下代碼模擬一個(gè)異步請(qǐng)求:
componentDidMount() {
setTimeout(() => {
this.setState({
text: 'fake request'
})
}, 5000);
}
導(dǎo)致這個(gè)問題的原因在于,當(dāng)異步請(qǐng)求完成,setState后App會(huì)re-render,而組件的componentWillReceiveProps會(huì)在父組件每次render的時(shí)候執(zhí)行,而此時(shí)傳入的user是一個(gè)空對(duì)象,所以UserInput的內(nèi)容被清空了。而getDerivedStateFromProps調(diào)用的更頻繁,會(huì)在組件每次render的時(shí)候調(diào)用,所以也會(huì)產(chǎn)生該問題。
為了解決這個(gè)問題我們可以在componentWillReceiveProps中判斷新傳入的user和當(dāng)前的user是否一樣,如果不一樣才設(shè)置state:
componentWillReceiveProps(nextProps) {
if (nextProps.user.id !== this.props.user.id) {
this.setState({
user: nextProps.user
});
}
}
更好的解決方案
派生狀態(tài)的數(shù)據(jù)源的不確定性會(huì)導(dǎo)致各種問題,那如果每份數(shù)據(jù)有且只被一個(gè)component管理應(yīng)該就能避免這些問題了。這種思路有兩種實(shí)現(xiàn),一種是數(shù)據(jù)完全由父組件管理,一種是數(shù)據(jù)完全由組件自己管理。下面分別討論:
完全受控組件(fully controlled component)
組件的數(shù)據(jù)完全來自于父組件,組件自己將不需要管理state。我們新建一個(gè)完全受控版的UserInput:
class FullyControlledUserInput extends React.Component {
render() {
const { user, onConfirm, onChange } = this.props;
return (
<div>
<input value={user.name || ''} onChange={onChange} />
<button onClick={() => { onConfirm(user) }}>確定</button>
</div>
)
}
}
App中調(diào)用FullyControlledUserInput的方法如下:
...
<FullyControlledUserInput
user={targetUser}
onChange={(e) => {
this.setState({
targetUser: {
id: targetUser.id,
name: e.target.value
}
});
}}
onConfirm={this.onConfirm}
/>
...
現(xiàn)在FullyControlledUserInput中的所有的數(shù)據(jù)都來源于父組件,由此解決數(shù)據(jù)沖突和被篡改的問題。
完全不受控組件(fully uncontrolled component)
組件的數(shù)據(jù)完全由自己管理,因此componentWillReceiveProps中的代碼都可以移除,但保留傳入props來設(shè)置state初始值:
class FullyUncontrolledUserInput extends React.Component {
state = {
user: this.props.user
}
onChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}
render() {
const { user } = this.state;
const { onConfirm } = this.props;
return (
<div>
<input value={user.name || ''} onChange={this.onChange} />
<button onClick={() => { onConfirm(user) }}>確定</button>
</div>
)
}
}
當(dāng)傳入的props發(fā)生改變時(shí),我們可以通過傳入一個(gè)不一樣的key來重新創(chuàng)建一個(gè)component的實(shí)例來實(shí)現(xiàn)頁面的更新。App中調(diào)用FullyUncontrolledUserInput的方法如下::
<FullyUncontrolledUserInput
user={targetUser}
onConfirm={this.onConfirm}
key={targetUser.id}
/>
大部分情況下,這是更好的解決方案?;蛟S有人會(huì)覺得這樣性能會(huì)受影響,其實(shí)性能并不會(huì)變慢多少,而且如果組件的更新邏輯過于復(fù)雜的話,還不如重新創(chuàng)建一個(gè)新的組件來的快。
在父組件中調(diào)用子組件的方法設(shè)置state
如果某些情況下沒有合適的屬性作為key,那么可以傳入一個(gè)隨機(jī)數(shù)或者自增的數(shù)字作為key,或者我們可以在組件中定義一個(gè)設(shè)置state的方法并通過ref暴露給父組件使用,比如我們可以在UserInput中添加:
setNewUserState = (newUser) => {
this.setState({
user: newUser
});
}
在App中通過ref調(diào)用這個(gè)方法:
...
<UserInput user={targetUser} onConfirm={this.onConfirm} ref='userInput' />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => {
this.setState({ targetUser: u });
this.refs.userInput.setNewUserState(u);
}}>
編輯
</button>
</li>
))
}
</ul>
<button onClick={() => {
this.setState({ targetUser: {} });
this.refs.userInput.setNewUserState({});
}}>
新建
</button>
...
這個(gè)方法不推薦使用,除非實(shí)在沒法了。。
本文源碼請(qǐng)參考:ways-to-update-component-on-props-change
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
React使用Electron開發(fā)桌面端的詳細(xì)流程步驟
React是一個(gè)流行的JavaScript庫,用于構(gòu)建Web應(yīng)用程序,結(jié)合Electron框架,可以輕松地將React應(yīng)用程序打包為桌面應(yīng)用程序,本文詳細(xì)介紹了使用React和Electron開發(fā)桌面應(yīng)用程序的步驟,需要的朋友可以參考下2023-06-06
nodejs和react實(shí)現(xiàn)即時(shí)通訊簡(jiǎn)易聊天室功能
這篇文章主要介紹了nodejs和react實(shí)現(xiàn)即時(shí)通訊簡(jiǎn)易聊天室功能,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08
詳解React Native開源時(shí)間日期選擇器組件(react-native-datetime)
本篇文章主要介紹了詳解React Native開源時(shí)間日期選擇器組件(react-native-datetime),具有一定的參考價(jià)值,有興趣的可以了解一下2017-09-09
React-redux實(shí)現(xiàn)小案例(todolist)的過程
這篇文章主要為大家詳細(xì)介紹了React-redux實(shí)現(xiàn)小案例(todolist)的過程,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09
webpack4+react多頁面架構(gòu)的實(shí)現(xiàn)
webpack在單頁面打包上應(yīng)用廣泛,以create-react-app為首的腳手架眾多。這篇文章主要介紹了webpack4+react多頁面架構(gòu)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10
React實(shí)現(xiàn)全局組件的Toast輕提示效果
這篇文章主要介紹了React實(shí)現(xiàn)全局組件的Toast輕提示效果,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09

