單向數據流
所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外改變父級組件的狀態,從而導致你的應用的數據流向難以理解。
額外的,每次父級組件發生更新時,子組件中所有的 prop 都將會刷新爲最新的值。這意味着你不應該在一個子組件內部改變 prop。
Props 的只讀性
決不能修改自身的 props。所有組件都必須保護它們的 props 不被更改。
State 與 props 類似,但是 state 是私有的,並且完全受控於當前組件。state 允許組件隨用戶操作、網絡響應或者其他變化而動態更改輸出內容。
讓組件在 props 變化時更新 state
在 vue 中我們會這樣做,定義一個本地的 data 屬性並將這個 prop 用作其初始值,然後通過 watch 監控 prop 變化,然後重複賦值給本地的 data 屬性。
props: ['initialCounter'],
data () {
return {
counter: this.props.initialCounter
}
},
watch: {
initialCounter (newCounter) {
if (newCounter !== this.counter) {
this.counter = newCounter
}
}
}
在 react 中,從 16.3 版本開始,當 props 變化時,建議使用新的 static getDerivedStateFromProps
生命週期更新 state。創建組件以及每次組件由於 props 或 state 的改變而重新渲染時都會調用該生命週期:
class ExampleComponent extends React.Component {
// 在構造函數中初始化 state,
// 或者使用屬性初始化器。
state = {
counter: this.props.initialCounter,
};
static getDerivedStateFromProps(props, state) {
if (props.initialCounter !== state.counter) {
return { counter: props.initialCounter };
}
// 返回 null 表示無需更新 state。
return null;
}
handleClick = () => {
// 點擊之後無法修改 counter 的 bug
this.setState({counter: this.state.counter + 1})
}
render() {
return (
<div onClick={this.handleClick}>{this.state.counter}</div>
);
}
}
getDerivedStateFromProps
會在調用 render 方法之前調用,並且在初始掛載及後續更新時都會被調用。它應返回一個對象來更新 state,如果返回 null 則不更新任何內容。
請注意,不管原因是什麼,都會在每次渲染前觸發此方法。這與 componentWillReceiveProps
形成對比,後者僅在父組件重新渲染時觸發,而不是在內部調用 setState 時。
基於 props 更新 state,舊的 componentWillReceiveProps
和新的 getDerivedStateFromProps
方法都會給組件增加明顯的複雜性。這通常會導致 bug。
最常見的誤解就是 getDerivedStateFromProps
和 componentWillReceiveProps
只會在 props “改變”時纔會調用。實際上只要父級重新渲染時,這兩個生命週期函數就會重新調用,不管 props 有沒有“變化”。所以,在這兩個方法內直接複製 props 到 state 是不安全的。這樣做會導致 state 後沒有正確渲染。
希望以上能解釋清楚爲什麼直接複製 prop 到 state 是一個非常糟糕的想法。
雖然這個設計就有問題,但是這樣的錯誤很常見,(我就犯過這樣的錯誤)。任何數據,都要保證只有一個數據來源,而且避免直接複製它。
讓我們看看一個相關的問題:假如我們只使用 props 中的 counter 屬性更新組件呢?
完全可控的組件
阻止上述問題發生的一個方法是,從組件裏刪除 state。然後傳入在父組件中定義的處理函數進行修改。
class ExampleComponent extends React.Component {
render() {
return (
<div onClick={this.props.handleClick}>
{this.props.initialCounter}
</div>
);
}
}
雖然 vue 中沒有這個問題,但是建議大家不要在組件裏面修改 props,任何數據,都要保證只有一個數據來源,而且避免直接複製它。都必須保護它們的 props 不被更改。
在一個受控組件中,表單數據是由 React 組件來管理的。另一種替代方案是使用非受控組件,這時表單數據將交由 DOM 節點來處理。
名詞“受控”和“非受控”通常用來指代表單的 inputs,但是也可以用來描述數據頻繁更新的組件。用 props 傳入數據的話,組件可以被認爲是受控(因爲組件被父級傳入的 props 控制)。數據只保存在組件內部的 state 的話,是非受控組件(因爲外部沒辦法直接控制 state)。
總結
派生狀態會導致代碼冗餘,並使組件難以維護。 確保你已熟悉這些簡單的替代方案:
1. 如果你需要執行副作用(例如,數據提取或動畫)以響應 props 中的更改,請改用 componentDidUpdate
。
class ExampleComponent extends React.Component {
state = {
counter: this.props.initialCounter
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}
componentDidUpdate(prevProps) {
if (this.props.initialCounter !== prevProps.initialCounter) {
this.setState({ counter: this.props.initialCounter })
}
}
render() {
return (
<div onClick={this.handleClick}>
{this.state.counter}
</div>
);
}
}
你也可以在 componentDidUpdate()
中直接調用 setState(),但請注意它必須被包裹在一個條件語句裏,正如上述的例子那樣進行處理,否則會導致死循環。它還會導致額外的重新渲染,雖然用戶不可見,但會影響組件性能。不要將 props “鏡像”給 state,請考慮直接使用 props。
3. 如果只想在 prop 更改時重新計算某些數據,請使用 memoization 幫助函數代替。
僅在輸入變化時,重新計算 render 需要使用的值————這個技術叫做 memoization 。(也就是Vue中的計算屬性類似)
import memoize from "memoize-one";
class Example extends Component {
// state 只需要保存當前的 filter 值:
state = { filterText: "" };
// 在 list 或者 filter 變化時,重新運行 filter:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// 計算最新的過濾後的 list。
// 如果和上次 render 參數一樣,`memoize-one` 會重複使用上一次的值。
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
在使用 memoization 時,請記住這些約束:
1、大部分情況下, 每個組件內部都要引入 memoized 方法,已免實例之間相互影響。
2、一般情況下,我們會限制 memoization 幫助函數的緩存空間,以免內存泄漏。(上面的例子中,使用 memoize-one 只緩存最後一次的參數和結果)。
4. 如果你想在 prop 更改時“重置”某些 state,請考慮使組件完全受控或使用 key 使組件完全不受控 代替。
1、完全可控的組件
class ExampleComponent extends React.Component {
render() {
return (
<div onClick={this.props.handleClick}>
{this.props.initialCounter}
</div>
);
}
}
2、有 key 的非可控組件
另外一個選擇是讓組件自己存儲臨時的 state。在這種情況下,組件仍然可以從 prop 接收“初始值”,但是更改之後的值就和 prop 沒關係了:
class ExampleComponent extends React.Component {
state = {
counter: this.props.initialCounter
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div onClick={this.handleClick}>
{this.state.counter}
</div>
);
}
}
// <ExampleComponent initialCounter={this.state.counter} key={this.state.key} />
我們可以使用 key 這個特殊的 React 屬性。當 key 變化時, React 會創建一個新的而不是更新一個既有的組件。
class ExampleComponent extends React.Component {
state = {
counter: this.props.initialCounter
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div onClick={this.handleClick}>
{this.state.counter}
</div>
);
}
}
class App extends React.Component {
state = {
counter: 0,
key: 0
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
key: this.state.key + 1,
})
}
render() {
return (
<div>
<button onClick={this.handleClick} >點擊</button>
<ExampleComponent
initialCounter={this.state.counter}
key={this.state.key} />
</div>
);
}
}
如果某些情況下 key 不起作用(可能是組件初始化的開銷太大),有兩種方法解決這個問題。
方法1:用 prop 的 ID 重置非受控組件
一個麻煩但是可行的方案是在 getDerivedStateFromProps
觀察 uuid 的變化:
class ExampleComponent extends React.Component {
// 在構造函數中初始化 state,
// 或者使用屬性初始化器。
state = {
counter: this.props.initialCounter,
prevPropsUuid: this.props.uuid
};
static getDerivedStateFromProps(props, state) {
// 觀察 uuid 的變化
if (props.uuid !== state.prevPropsUuid) {
return {
counter: props.initialCounter,
prevPropsUuid: props.uuid
};
}
// 返回 null 表示無需更新 state。
return null;
}
handleClick = () => {
this.setState({counter: this.state.counter + 1})
}
render() {
return (
<div onClick={this.handleClick}>={this.state.counter}</div>
);
}
}
class App extends React.Component {
state = {
counter: 0,
key: 0
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
key: this.state.key + 1,
})
}
render() {
return (
<div>
<button onClick={this.handleClick} >點擊</button>
<ExampleComponent
initialCounter={this.state.counter}
uuid={this.state.key} />
</div>
);
}
}
方法2:使用實例方法重置非受控組件
在組件上使用 ref 可以獲取組件實例:
class ExampleComponent extends React.Component {
// 在構造函數中初始化 state,
// 或者使用屬性初始化器。
state = {
counter: this.props.initialCounter
};
handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}
resetCounter (newCounter) {
this.setState({ counter: newCounter });
}
render() {
return (
<div onClick={this.handleClick}>={this.state.counter}</div>
);
}
}
class App extends React.Component {
state = {
counter: 0,
key: 0
}
componentRef = React.createRef()
handleClick = () => {
let newCounter = this.state.counter + 1
this.setState({ counter: newCounter })
this.componentRef.current.resetCounter(newCounter)
}
render() {
return (
<div>
<button onClick={this.handleClick} >點擊</button>
<ExampleComponent
initialCounter={this.state.counter}
ref={this.componentRef} />
</div>
);
}
}
refs 在某些情況下很有用,比如這個。但通常我們建議謹慎使用。即使是做一個演示,這個命令式的方法也是非理想的,因爲這會導致兩次而不是一次渲染。
概括
回顧一下,設計組件時,重要的是確定組件是受控組件還是非受控組件。
不要直接複製 props 的值到 state 中,而是去實現一個受控的組件,然後在父組件裏合併兩個值。
對於不受控的組件,當你想在 prop 變化(通常是 ID )時重置 state 的話,可以選擇一下幾種方式:
1、建議: 重置內部所有的初始 state,使用 key 屬性
2、選項一:僅更改某些字段,觀察特殊屬性的變化(比如 props.uuid)。
3、選項二:使用 ref 調用實例方法。
最後提一下,上面是 react 的寫法,再介紹一下 vue 的最佳寫法使用語法糖 v-model。
<template>
<div class="hello">
<h1 @click="handleClick">{{ value }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['value'],
data() {
return {
counter: this.value
};
},
methods: {
handleClick () {
this.$emit('input', this.value + 1)
}
}
}
</script>
// <HelloWorld v-model="initialCounter"/>