1. 程式人生 > 實用技巧 >React 【認識 State】定義state、修改state、state的不可變原則、state的props的區別

React 【認識 State】定義state、修改state、state的不可變原則、state的props的區別

目錄:

1. 如何定義 State

2. 如何修改 State

  使用 setState

  setState 是非同步的

  State 的更新是一個淺合併的過程

3. State 的不可變原則

4. State 和 props 的區別

  React 的核心思想是元件化的思想,應用由元件搭建而成。元件中最重要的概念是 State,State 是元件的 UI 資料模型,是元件渲染時的一個數據依據。

一、如何定義 State

  State 代表元件 UI 狀態的變化。

  在 React 應用中,當涉及到元件 UI 變化的時候,必須要將變數定義成 State 值。

  定義 State 值的方式可以是在 constructor 中初始化 state值。

例 子

  父元件是 App.js,子元件是 listItem.jsx。目前在頁面上點選“+”“-”按鈕可以改變 count 的值,但是 UI 不會變化。

import React, { Component } from 'react';
import ListItem from './components/listItem'

const listData = [
    {
        id: 1,
        name: '紅蘋果',
        price: 2
    },
    {
        id: 2,
        name: '青蘋果',
        price: 3
    },
]

class App extends Component {
    renderList(){
        return listData.map( item => {
            return(
                
<ListItem key={item.id} data={ item } onDelete={this.handleDelete} /> ) }) } handleDelete = (id) => { console.log( 'id:', id ); } render() { return(
<div className="container"> { listData.length === 0 && <div className="text-center">購物車是空的</div> } { this.renderList() } </div> ) } } export default App;
App.js
import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'

const cls = classnames.bind(style);

let count = 0;

class ListItem extends Component {
    doSomethingWithCount(){
        if(count<0){
            count = 0
        }
    }
    handleDecrease = () => {
        count --;
        this.doSomethingWithCount();
        console.log( count );
    }
    handleIncrease = () => {
        count ++;
        console.log( count );
    }
    render() { 
        return ( 
            <div className="row mb-3">
                <div className="col-6 themed-grid-col">
                    <span className={ cls('title', 'list-title') }>
                        {this.props.data.name}
                    </span>
                </div>
                <div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
                <div className={`col-2 themed-grid-col${count ? '' : '-s'}`}>
                    <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
                    <span className={ cls('digital') }>{count}</span>
                    <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
                </div>
                <div className="col-1 themed-grid-col">
                    <button 
                    onClick={()=>{this.props.onDelete(this.props.data.id)}} 
                        className="btn btn-danger btn-sm" 
                        type="button"
                    >刪除
                    </button>
                </div>
            </div>
        );
    }
}

export default ListItem;
listItem.jsx

頁面表現:

  為了讓 count 在 UI 上發生變化,在 listItem.jsx 中定義 State。在 constructor 裡使用 this.state,賦給 this.state 一個物件,並將變數 count 寫到物件裡。

  引用 state 的時候,不能直接使用 count ,而是應該使用 this.state.count 去引用。

例子

  下面程式碼演示如何將變數 count 定義為 state。

  ( 因為在幾個函式裡對 count 的修改方式錯誤,所以程式碼會報錯,在筆記下面有修改 state 的部分。)

import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'

const cls = classnames.bind(style);

class ListItem extends Component {
    constructor(props){
        super(props)
        this.state = {
            count : 0
        }
    }
    doSomethingWithCount(){
        if(count<0){
            count = 0
        }
    }
    handleDecrease = () => {
        count --;
        this.doSomethingWithCount();
        console.log( count );
    }
    handleIncrease = () => {
        count ++;
        console.log( count );
    }
    render() { 
        return ( 
            <div className="row mb-3">
                <div className="col-6 themed-grid-col">
                    <span className={ cls('title', 'list-title') }>
                        {this.props.data.name}
                    </span>
                </div>
                <div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
                <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
                    <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
                    <span className={ cls('digital') }>{this.state.count}</span>
                    <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
                </div>
                <div className="col-1 themed-grid-col">
                    <button 
                    onClick={()=>{this.props.onDelete(this.props.data.id)}} 
                        className="btn btn-danger btn-sm" 
                        type="button"
                    >刪除
                    </button>
                </div>
            </div>
        );
    }
}
listItem.jsx

怎麼樣定義需要一個 State

  定義 State 值之前需要做一些思考。

  第一、判斷這個狀態是否需要通過 props 從父元件獲取,從父元件中獲取並且在整個元件中不發生變化,如果是這樣的話,可能就不需要去定義 state。第二、判斷是否可以通過其它 state 和 props 計算得到,如果可以,可能就不需要去定義 state。第三、判斷是否在 render 方法中使用,如果不在 render 方法中使用,可能就不需要去定義 state。

  比如上面的例子,將 count 定義成 state,是因為 count 不能從父元件中獲取,使用者的互動使 count 發生變化,並且在 render 方法中會使用到 count。

二、如何修改 State

使用 setState

  下面說明如何修改 state 的值,從而改變 UI 的狀態。

例 子

  在 “-”、“+” 的 button 元素上添加了點選事件,在事件回撥函式中對變數 count 進行操作。如果 count 是 state 值,只能通過 setState 方法對 count 進行修改。在本例中,給 setState 方法賦一個物件作為引數,在物件中修改 count 的值。

import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'

const cls = classnames.bind(style);

class ListItem extends Component {
    constructor(props){
        super(props)
        this.state = {
            count : 0
        }
    }
    handleDecrease = () => {
        this.setState({
            count : this.state.count - 1
        })
    }
    handleIncrease = () => {
        this.setState({
            count : this.state.count + 1
        })
    }
    render() { 
        return ( 
            <div className="row mb-3">
                <div className="col-6 themed-grid-col">
                    <span className={ cls('title', 'list-title') }>
                        {this.props.data.name}
                    </span>
                </div>
                <div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
                <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
                    <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
                    <span className={ cls('digital') }>{this.state.count}</span>
                    <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
                </div>
                <div className="col-1 themed-grid-col">
                    <button 
                    onClick={()=>{this.props.onDelete(this.props.data.id)}} 
                        className="btn btn-danger btn-sm" 
                        type="button"
                    >刪除
                    </button>
                </div>
            </div>
        );
    }
}

export default ListItem;
listItem.jsx

頁面表現:

初學者可能會犯的錯誤

  錯誤1. 通過類似“ this.state.count -- “ 這樣的方式修改 state 值,這完全沒有作用。

  錯誤2. 在 constructor 中使用 setState 方法進行狀態改變,這會報錯。

setState 是非同步的

  這是因為 React 執行 setState 的時候會優化 setState 執行的時機,有可能會因為效能優化將多個 setState 合併在一起去執行,所以 setState 不是同步執行的。所以,不要依賴當前的 state 去計算另外一個 state ,如果必須要拿到修改完成之後的 state 可以通過回撥函式的方式去實現。

例 子

  點選 “ + ” 按鈕時,會呼叫 handleIncrease 函式,在函式中使用 setState 方法修改 count ,讓 count 加 1。為了演示 setState 是非同步的,在使用 setState 修改 count 前後新增標記,輸出 count。

import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'

const cls = classnames.bind(style);

class ListItem extends Component {
    constructor(props){
        super(props)
        this.state = {
            count : 0
        }
    }
    handleDecrease = () => {
        this.setState({
            count : this.state.count - 1
        })
    }
    handleIncrease = () => {
        console.log('step 1', this.state.count);
        this.setState({
            count : this.state.count + 1
        })
        console.log('step 2', this.state.count);
    }
    render() { 
        return ( 
            <div className="row mb-3">
                <div className="col-6 themed-grid-col">
                    <span className={ cls('title', 'list-title') }>
                        {this.props.data.name}
                    </span>
                </div>
                <div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
                <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
                    <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
                    <span className={ cls('digital') }>{this.state.count}</span>
                    <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
                </div>
                <div className="col-1 themed-grid-col">
                    <button 
                    onClick={()=>{this.props.onDelete(this.props.data.id)}} 
                        className="btn btn-danger btn-sm" 
                        type="button"
                    >刪除
                    </button>
                </div>
            </div>
        );
    }
}

export default ListItem;
listItem.jsx

頁面表現:

  點選按鈕在控制檯輸出的 count 均為 0 ,說明 setState 是非同步的,因為如果不是非同步的話 step 2 輸出的 count 應該是 1。

  如果必須要使用變化後的 state 值進行下一步的操作,可以給 setState 方法傳入第二個引數,可以傳入一個回撥函式,這個回撥函式執行的時機就是 setState 已經執行完成的一個時機。

例 子

import React, { Component } from 'react';
import style from './listItem.module.css';
import classnames from 'classnames/bind'

const cls = classnames.bind(style);

class ListItem extends Component {
    constructor(props){
        super(props)
        this.state = {
            count : 0
        }
    }
    handleDecrease = () => {
        this.setState({
            count : this.state.count - 1
        })
    }
    handleIncrease = () => {
        console.log('step 1', this.state.count);
        this.setState({
            count : this.state.count + 1
        }, ()=>{
            console.log('step 3', this.state.count);
        })
        console.log('step 2', this.state.count);
    }
    render() { 
        return ( 
            <div className="row mb-3">
                <div className="col-6 themed-grid-col">
                    <span className={ cls('title', 'list-title') }>
                        {this.props.data.name}
                    </span>
                </div>
                <div className="col-1 themed-grid-col">¥{this.props.data.price}</div>
                <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}>
                    <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button>
                    <span className={ cls('digital') }>{this.state.count}</span>
                    <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button>
                </div>
                <div className="col-1 themed-grid-col">
                    <button 
                    onClick={()=>{this.props.onDelete(this.props.data.id)}} 
                        className="btn btn-danger btn-sm" 
                        type="button"
                    >刪除
                    </button>
                </div>
            </div>
        );
    }
}

export default ListItem;
listItem.jsx

頁面表現:

  點選按鈕之後,可以看到 step 3 輸出的 count 值加了 1。

state 的更新是一個淺合併(shallow merge)的過程

例 子

  如果 state 有多個屬性,比如說有 count、price。在只需要修改 count 而不需要修改 price 時,只需要給 setState 傳入 count 即可,不需要傳入整個 state。

三、State 的不可變原則

在建立新狀態的時候要遵循一些原則。

當狀態發生變化的時候,根據狀態的型別可以分為以下三種情況:

1. 值型別 : 數字、字串、布林值、null、undefined

2. 陣列型別

3. 物件

建立新的狀態

1. 狀態的型別是值型別

  狀態是不可變型別,直接給要修改的狀態賦予新值即可。使用 setState 的時候可以直接修改 state 值,比如說數字、字串、布林值等。

2. 狀態的型別是陣列型別

  有 2 種方法:① concat;② 使用“...”結構符去生成新陣列

① concat

  concat 方法用於合併兩個或多個數組,此方法不會更改原來的陣列,而是會返回一個新陣列。

  注意,不要使用諸如 push 、pop、shift、unshift、spice 等方法,因為這些方法都是在原陣列的基礎上修改的。可以使用 concat、slice、filter 這些方法,這些方法會返回一個新陣列。

② 使用 “...” 結構符去生成新陣列

  使用 “...” 結構符去生成新陣列原理跟使用 concat 是一樣的。生成 _books 新陣列之後,使用 setState 將 _books 賦予給原來的狀態。

3. 狀態的型別是物件

  通常會使用 Object.assign 方法生成新物件,Object.assign 用於將所有可列舉屬性的值從一個或多個物件複製到目標物件。

  如果要進行深拷貝,還是要使用 JSON.stringify() 等方法。

① Object.assign

  下面例子使用 Object.assign 合併物件 {}、this.state.item、{price:9000},將 this.state.item 跟 {price:9000} 合併到空物件 { } 中,從而去生成一個新物件。

② “...” 結構符

  使用 “...” 結構符道理跟使用 Object.assign 是一樣的。

小結

  建立新的狀態物件的關鍵是避免使用會修改原物件的方法,而是使用可以返回一個新物件的方法。推薦使用類似 Immutable 這樣的 JS 庫,可以方便地建立和管理不可變的資料。

四、State 和 props 的區別

State

1. 可變的

  state 用於元件儲存、控制、修改

2. 元件內部

  state 在元件內部初始化,可以被元件自身修改,但是外部不能訪問也不能修改,所以,可以認為 state 是一個區域性只能被元件自身控制的資料來源

3. 互動或其他 UI 造成的資料更新

  state 是互動或其他 UI 造成的資料更新。通常情況下,state 的這種變化會觸發元件的重新渲染。

Props

1. 在元件內部不可變

  props 是父元件傳入的引數物件,它是外部傳來的配置引數。

2. 父組傳入

  在元件內部無法控制也無法修改,除非外部元件主動傳入新的 props,否則元件的 props 保持不變。

3. 簡單的資料流

  可以把 props 看成是一個從上而下的一個簡單的資料流。

state 跟 props 的聯絡

  元件中 state 的資料可以通過 props 傳入子元件,反過來,元件也可以用外部傳入的 props 來初始化自己的 state。

  state 是讓元件控制自己的狀態,而 props 是讓外部對元件進行配置。