React學習:狀態(State) 和 屬性(Props)
React :元素構成元件,元件又構成應用。
React核心思想是元件化,其中 元件 通過屬性(props) 和 狀態(state)傳遞資料。
State 與 Props 區別
props 是元件對外的介面,state 是元件對內的介面
。元件內可以引用其他元件,元件之間的引用形成了一個樹狀結構(元件樹),如果下層元件需要使用上層元件的資料或方法,上層元件就可以通過下層元件的props屬性進行傳遞,因此props是元件對外的介面。元件除了使用上層元件傳遞的資料外,自身也可能需要維護管理資料,這就是元件對內的介面state。根據對外介面props 和對內介面state,元件計算出對應介面的UI。
主要區別:
- State是可變的,是一組用於反映元件UI變化的狀態集合;
- 而Props對於使用它的元件來說,是隻讀的,要想修改Props,只能通過該元件的父元件修改。
在元件狀態上移的場景中,父元件正是通過子元件的Props, 傳遞給子元件其所需要的狀態。
>
Props的使用
當一個元件被注入一些屬性(Props )值時,屬性值來源於它的父級元素,所以人們常說,屬性在 React 中是單向流動的:從父級到子元素。
1、props(屬性) 預設為 “true”
如果你沒給 prop(屬性) 傳值,那麼他預設為 true 。下面兩個 JSX 表示式是等價的:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
通常情況下,我們不建議使用這種型別,因為這會與ES6中的物件shorthand混淆 。ES6 shorthand 中 {foo} 指的是 {foo: foo} 的簡寫,而不是 {foo: true} 。這種行為只是為了與 HTML 的行為相匹配。
(舉個例子,在 HTML 中,< input type=”radio” value=”1” disabled /> 與 < input type=”radio” value=”1” disabled=”true” /> 是等價的。JSX 中的這種行為就是為了匹配 HTML 的行為。)
2、props擴充套件
如果你已經有一個 object 型別的 props,並且希望在 JSX 中傳入,你可以使用擴充套件操作符 … 傳入整個 props 物件。這兩個元件是等效的:
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
顯然下面的方法更方便:因為它將資料進行了包裝,而且還簡化了賦值的書寫
State
一、State是什麼?
React 的核心思想是元件化
,而元件中最重要的概念是State(狀態),State是一個元件的UI資料模型,是元件渲染時的資料依據。
狀態(state) 和 屬性(props) 類似,都是一個元件所需要的一些資料集合,但是state是私有的,可以認為state是元件的“私有屬性(或者是區域性屬性)”。
如何判斷是否為State ?
元件中用到的一個變數是不是應該作為元件State,可以通過下面的4條依據進行判斷:
- 這個變數是否是通過Props從父元件中獲取?如果是,那麼它不是一個狀態。
- 這個變數是否在元件的整個生命週期中都保持不變?如果是,那麼它不是一個狀態。
- 這個變數是否可以通過其他狀態(State)或者屬性(Props)計算得到?如果是,那麼它不是一個狀態。
- 這個變數是否在元件的render方法中使用?如果不是,那麼它不是一個狀態。這種情況下,這個變數更適合定義為元件的一個普通屬性,例如元件中用到的定時器,就應該直接定義為this.timer,而不是this.state.timer。
並不是元件中用到的所有變數都是元件的狀態!
當存在多個元件共同依賴一個狀態時,一般的做法是狀態上移,將這個狀態放到這幾個元件的公共父元件中。
二、如何正確使用 State
1、用setState 修改State
直接修改state,元件並不會重新觸發render()
// 錯誤
this.state.comment = 'Hello';
正確的修改方式是使用setState()
// 正確
this.setState({comment: 'Hello'});
2、State 的更新是非同步的
- 呼叫setState後,setState會把要修改的狀態放入一個佇列中(因而 元件的state並不會立即改變);
- 之後React 會優化真正的執行時機,來優化效能,所以優化過程中有可能會將多個 setState 的狀態修改合併為一次狀態修改,因而state更新可能是非同步的。
- 所以不要依賴當前的State,計算下個State。當真正執行狀態修改時,依賴的this.state並不能保證是最新的State,因為React會把多次State的修改合併成一次,這時,this.state將還是這幾次State修改前的State。
另外需要注意的事,同樣不能依賴當前的Props計算下個狀態,因為Props一般也是從父元件的State中獲取,依然無法確定在元件狀態更新時的值。
綜上所述:
this.props 和 this.state 可能是非同步更新的,你不能依賴他們的值計算下一個state(狀態)
例:
這樣 counter(計數器)會更新失敗
// 錯誤
this.setState({
counter: this.state.counter + this.props.increment,
});
要彌補這個問題,使用 setState() 的另一種形式,它接受一個函式而不是一個物件。這個函式有兩個引數:
(1)第一個引數: 是當前最新狀態的前一個狀態(本次元件狀態修改前的狀態)
(2)第二個引數:是當前最新的屬性props
// 正確
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
//注意:下面這樣是錯的
this.setState((prevState, props) => { //沒將{}用()括起來,所以會解析成程式碼塊
counter: prevState.counter + props.increment
});
如果你還不懂沒關係,看下面例子:
我們現在渲染出一個button,想每點選一下,counter就+3
看下面程式碼:
class App extends React.Component {
state = {
counter: 0,
}
handleClick = () => {
const { counter } = this.state;
//或者 const counter = this.state.counter;
this.setState({ counter: counter + 1 });
this.setState({ counter: counter + 1 });
this.setState({ counter: counter + 1 });
}
render() {
return (
<div>
counter is: {this.state.counter}
<button onClick={this.handleClick} >點我</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
輸出:
每點選一下,加+1,並不是+3
之所以+1,不是+3,是因為 state 的更新可能是非同步的,React 會把傳入多個 setState的多個 Object “batch” 起來合併成一個。合併成一個就相當於把傳入 setState 的多個 Object 進行 shallow merge
,像這樣:
const update = {
counter: counter + 1,
counter: counter + 1,
counter: counter + 1
//因為上面三句話都一樣,所以會當一句話執行
}
我們可以這麼做就會成功:看下面
class App extends React.Component {
state = {
counter: 0,
}
handleClick = () => {
this.setState(prev => ({ counter: prev.counter + 1 }));
this.setState(prev => ({ counter: prev.counter + 1 }));
this.setState(prev => ({ counter: prev.counter + 1 }));
//這樣是錯的 this.setState(prev => {counter: prev.counter + 1});
//這樣是錯的 this.setState(prev => {counter:++prev.counter});
//這樣是錯的 this.setState(prev => {counter:prev.counter++});
}
render() {
return (
<div>
counter is: {this.state.counter}
<button onClick={this.handleClick} >點我</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
之所以成功是因為:傳入多個 setState 的多個 Object 會被 shallow Merge,而傳入多個 setState 的多個 function 會被 "queue" 起來,queue 裡的 function 接收到的 state(上面是 prev )都是前一個 function 操作過的 state。
3、State更新會被合併
官方文件看不懂不要緊,直接舉個例子你就懂了。
例如一個元件的狀態為:
this.state = {
title : 'React',
content : 'React is an wonderful JS library!'
}
當只需要修改狀態title時,只需要將修改後的title傳給setState:
this.setState({title: 'Reactjs'});
React會合並新的title到原來的元件狀態中,同時保留原有的狀態content,合併後的State為:
{
title : 'Reactjs',
content : 'React is an wonderful JS library!'
}
三、根據State型別 更新
當狀態發生變化時,如何建立新的狀態?根據狀態的型別,可以分成三種情況:
1、 狀態的型別是不可變型別(數字,字串,布林值,null, undefined)
這種情況最簡單,直接給要修改的狀態賦一個新值即可
//原state
this.state = {
count: 0,
title : 'React',
success:false
}
//改變state
this.setState({
count: 1,
title: 'bty',
success: true
})
2、 狀態的型別是陣列
陣列是一個引用,React 執行 diff 演算法時比較的是兩個引用,而不是引用的物件。所以直接修改原物件,引用值不發生改變的話,React 不會重新渲染。因此,修改狀態的陣列或物件時,要返回一個新的陣列或物件。
(1)增加
如有一個數組型別的狀態books,當向books中增加一本書(chinese)時,使用陣列的concat方法或ES6的陣列擴充套件語法
// 方法一:將state先賦值給另外的變數,然後使用concat建立新陣列
let books = this.state.books;
this.setState({
books: books.concat(['chinese'])
})
// 方法二:使用preState、concat建立新陣列
this.setState(preState => ({
books: preState.books.concat(['chinese'])
}))
// 方法三:ES6 spread syntax
this.setState(preState => ({
books: [...preState.books, 'chinese']
}))
(2)擷取
當從books中擷取部分元素作為新狀態時,使用陣列的slice方法:
// 方法一:將state先賦值給另外的變數,然後使用slice建立新陣列
let books = this.state.books;
this.setState({
books: books.slice(1,3)
})
//
// 方法二:使用preState、slice建立新陣列
this.setState(preState => ({
books: preState.books.slice(1,3)
}))
(3)條件過濾
當從books中過濾部分元素後,作為新狀態時,使用陣列的filter方法:
// 方法一:將state先賦值給另外的變數,然後使用filter建立新陣列
var books = this.state.books;
this.setState({
books: books.filter(item => {
return item != 'React';
})
})
// 方法二:使用preState、filter建立新陣列
this.setState(preState => ({
books: preState.books.filter(item => {
return item != 'React';
})
}))
注意:不要使用push、pop、shift、unshift、splice等方法修改陣列型別的狀態,因為這些方法都是在原陣列的基礎上修改,而concat、slice、filter會返回一個新的陣列。
3、狀態的型別是普通物件(不包含字串、陣列)
物件是一個引用,React 執行 diff 演算法時比較的是兩個引用,而不是引用的物件。所以直接修改原物件,引用值不發生改變的話,React 不會重新渲染。因此,修改狀態的陣列或物件時,要返回一個新的物件。
使用ES6 的Object.assgin方法
// 方法一:將state先賦值給另外的變數,然後使用Object.assign建立新物件
var owner = this.state.owner;
this.setState({
owner: Object.assign({}, owner, {name: 'Jason'})
})
// 方法二:使用preState、Object.assign建立新物件
this.setState(preState => ({
owner: Object.assign({}, preState.owner, {name: 'Jason'})
}))
使用物件擴充套件語法(object spread properties)
// 方法一:將state先賦值給另外的變數,然後使用物件擴充套件語法建立新物件
var owner = this.state.owner;
this.setState({
owner: {...owner, name: 'Jason'}
})
// 方法二:使用preState、物件擴充套件語法建立新物件
this.setState(preState => ({
owner: {...preState.owner, name: 'Jason'}
}))
綜上所述:
建立新的狀態物件的關鍵是,避免使用會直接修改原物件的方法,而是使用可以返回一個新物件的方法。
四、State向下流動
我們說 props 是元件對外的介面,state 是元件對內的介面。
一個元件可以選擇將 state(狀態) 向下傳遞,作為其子元件的 props(屬性):
<MyComponent title={this.state.title}/>
這通常稱為一個“從上到下”,或者“單向”的資料流。任何 state(狀態) 始終由某個特定元件所有,並且從該 state(狀態) 匯出的任何資料 或 UI 只能影響樹中 “下方” 的元件。
如果把元件樹想像為 props(屬性) 的瀑布,所有元件的 state(狀態) 就如同一個額外的水源匯入主流,且只能隨著主流的方向向下流動。