iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 21
0
Modern Web

給初入JS框架新手的React.js入門系列 第 21

【React.js入門 - 21】 各階層Component的溝通

在使用component時,我們常會遇到不同元件需要互相傳資料、呼叫函式的情況,大致可分為:

  1. 子對父
  2. 父對子
  3. 子對子(同父的子類別之間)
  4. 祖先和某代孫子之間

自己注意到身旁剛接觸js框架或是對於物件導向階層不熟的朋友,在操作各層元件的溝通時比較難快速上手,所以特地整理了這篇。

我想把所有溝通都整理在一起,所以這一篇很長、超長,要有心理準備QQ

我們將會在這樣的javasript的程式階層下講解:

index.js
    |____ App.js
          |____ Parent.js
                |____ Sister.js
                |____ Brother.js

並以class component作為講解的架構。

在某個地方,有一對很開明的父母,育有一對子女。只要子女有求他們必應。
有一天孩子長大了,他們決定以總金額100分配給他們兩個零用錢,不過對兒子和女兒兩個人而言,他們各自期望著不同的金額。一開始,他們讓兒子分到60塊。女兒分到40塊,並且先問了女兒的意見。

(寫完的時候才發現命名用daughter和son好像比較好哈哈,不過圖都畫了就算了。)
在接下來的說明中,【開始】是表示目前的步驟開始實現標題所指定的行為。不是代表說程式碼寫了【開始】之前的東西後面的東西就會自動跑。

Part.1 - 子對父

Part.1-1 - 在子元件主動使用父元素的資料/函式

就是props,不解釋

Part.1-2 - 在子元件主動修改綁定在自己身上的props

子元素不能直接修改props的值(read-only),下面這樣的函式就是錯的:

this.props.money= 70;

如果要修改props,只能照以下步驟:

-----------------【準備】-----------------

  1. 在父元素定義一個函式,用來「修改綁定在子元素props上的state」 (或是純用來接收資料)
  2. 綁定該函式在子元素props上

-----------------【開始】-----------------

  1. 在子元素中呼叫該函式

-----------------【結束】-----------------

在子元件傳資料給父元件/透過父元件函式更改父元件也是用同樣的方法。
例如Parent中的allocateMoney:

import React, { Component } from 'react';
import Brother from "./Brother";
import Sister from "./Sister";
class Parent extends Component{
  constructor(props) {
    super(props);
    this.state={ 
      moneyForBrother:60,
      moneyForSister:40
    }
    this.allocateMoney=this.allocateMoney.bind(this);
  }

    allocateMoney(target,amount){
        if(target==="brother")
            this.setState({moneyForBrother:amount})
        else
            this.setState({moneyForSister:amount});
    }

    render(){
        return(
          <div>
              <Brother money={this.state.moneyForBrother} argue={this.allocateMoney} />
              <Sister money={this.state.moneyForSister} argue={this.allocateMoney}/>
          </div>
        );
    }
}
export default Parent;

這對父母綁定了讓兒女和他們討論的方法allocateMoney在argue上。
於是女兒使用了this.props.argue(70);呼叫父母給的函式,把自己的錢提升到70塊

/* Sister.js */

import React, { Component } from 'react';
class Sister extends Component{
  constructor(props) {
    super(props);
    this.state={ // 宣告state物件,內包含一個變數percent
      feeling:"bad" 
    }
    this.argueFor70NTD=this.argueFor70NTD.bind(this);
  }

    componentDidMount(){
    }

    argueFor70NTD(){
        this.props.argue("sister",70);
    }

    render(){
        return(<div>我是女兒,我拿到{this.props.money}<button onClick={this.argueFor70NTD}>要求提升到70塊</button></div>);
    }
}
export default Sister;


當你按下女兒的吵架用按鍵,女兒的props就會被改變(錢就會變70塊)

邏輯圖(順序由深至淺):

Part.2 - 父對子

Part.2-1 在父元件主動觸發子元件的函式

父元件是沒有辦法直接觸發子元件的函式的,必須透過以下步驟
-----------------【準備】-----------------

  1. 綁定一個state在子元件props上

-----------------【開始】-----------------

  1. 改變綁定在子元件props上的state
  2. 讓子元素監測到特定props被改變後觸發函式

-----------------【結束】-----------------

例如如果今天兒子的情緒表現是一個函式(Brother.js所有的程式碼等等會有):

    setMyFeeling(){
        if(this.props.money<40)
            this.setState({feeling:"不能接受"})
        else
            this.setState({feeling:"可以接受"})
    }

而要讓他產生對於錢的心情,我們要在剛剛的Parent.js中加上:

<Brother money={this.state.moneyForBrother} />

讓父母告訴兒子他的錢,然後讓兒子在componentDidUpdate中根據錢的更動做出反應:

import React, { Component } from 'react';
class Brother extends Component{
  constructor(props) {
    super(props);
    this.state={ 
      feeling:"?",
      expectAmount: 40
    }
    this.setMyFeeling=this.setMyFeeling.bind(this);
  }

    setMyFeeling(){
        if(this.props.money<this.state.expectAmount)
            this.setState({feeling:"不能接受"})
        else
            this.setState({feeling:"可以接受"})
    }

    componentDidUpdate(prevProps, prevState, snapshot){
        if(prevProps.money!==this.props.money){
            this.setMyFeeling();
        }
    }

    render(){
        return(<div>我是兒子,我拿到{this.props.money},我目前{this.state.feeling}</div>);
    }
}
export default Brother;

要注意要用if(prevProps.money!==this.props.money)去限制呼叫函式的時機,否則在setMyFeeling();中進行setState時會再次觸發componentDidUpdate造成無限呼叫setMyFeeling();的迴圈。

邏輯圖(順序由深至淺):

Part.2-2 - 在父元件主動取得子元素的資料

一樣的,父元素沒辦法直接主動取得子元素中的資料。但我們可以結合前面:
在父元件主動取得子元素的資料 = 在父元件主動觸發子元件的函式 + 在子元件主動傳資料給父元件
也就是:
-----------------【準備】-----------------

  1. 在父元素定義一個函式,用來接收參數,綁在子元件上 (如: allocateMoney)
  2. 綁定一個state在子元件props上 (EX: money)

-----------------【開始】-----------------

  1. 父元素改變剛剛綁定在子元件props上的state (EX: money)
  2. 讓子元素監測到特定props被改變後觸發剛剛綁的函式,把資料(EX: expectAmount)傳入該函式參數
  3. 父元素透過子元素呼叫的父元素函式取得資料

-----------------【結束】-----------------

例如,我們先綁定allocateMoney給Brother

<Brother money={this.state.moneyForBrother} argue={this.allocateMoney}/>

然後在Brother剛剛的函式中加上this.props.argue("brother",this.state.expectAmount):

    setMyFeeling(){
        if(this.props.money<40)
            this.setState({feeling:"不能接受"})
        else
            this.setState({feeling:"可以接受"})
        this.props.argue("brother",this.state.expectAmount);
    }

就等同於

  1. 我們定義好接收money的allocateMoney()
  2. 我們改變了Parent給Brother的Money
  3. Brother偵測到Money被改變,以expectAmount為參數來呼叫argue()(透過setMyFeeling()間接呼叫)
  4. Parent透過綁定在上的argue()allocateMoney()取得Brother的expectAmount

也就是我們用Parent主動取得了Brother期望的錢。

邏輯圖(順序由深至淺):

Part.3 - 子對子(同父的子類別之間)

實際上剛剛的過程跑完,我們就已經完成同階層子對子的溝通了。
我們來解釋一下:

Part.3-1 - 同一父元件A中,子元件B主動修改子元件C的props

-----------------【準備】-----------------

  1. 用A的state來綁定C的props,在A中定義一個改變該state的函式

    綁在Brother上的Money & Parent的allocateMoney

  2. 把該函式綁在B上

    綁在Sister上的allocateMoney(argue)

-----------------【開始】-----------------
(按下Sister按鍵)

  1. 在B元件呼叫綁定在props上的A元件函式

    Sister呼叫argue(allocateMoney)

  2. A元件的函式修改了綁在C元件上的state,C元件的props就會被改變

    Parent的allocateMoney改變了Money,綁在Brother上的Money被改變

-----------------【結束】-----------------

邏輯圖(順序由深至淺):

Part.3-2 -同一父元件A中,子元件B主動呼叫子元件C的函式

-----------------【準備】-----------------

  1. 用A的state來綁定C的props,在A中定義一個改變該state的函式

    綁在Brother上的Money & Parent的allocateMoney

  2. 把該函式綁在B上

    綁在Sister上的allocateMoney(argue)

-----------------【開始】-----------------
(按下Sister按鍵)

  1. 在B元件呼叫綁定在props上的A元件函式

    Sister呼叫argue(allocateMoney)

  2. A元件的函式修改了綁在C元件上的state,C元件的props就會被改變

    Parent的allocateMoney改變了Money,綁在Brother上的Money被改變

  3. C元件的偵測到props被改變,呼叫對應C元件函式

    Brother偵測到Money改變,呼叫setMyFeeling()

-----------------【結束】-----------------

邏輯圖(順序由深至淺):

Part.3-3 - 同一父元件A中,子元件B主動取得子元件C的資料

這裡,我們只要把兒子的錢也綁在女兒上就完成了:

<Sister money={this.state.moneyForSister} moneyBrother={this.state.moneyForBrother} argue={this.allocateMoney}/>

-----------------【準備】-----------------

  1. 用A的state來綁定B和C的props,在A中對兩個state定義會改變個別state的函式

    綁在Sister上的MoneyBrother & 綁在Brother上的Money & Parent的allocateMoney
    (這個例子中,此函式兩個state都能設定)

  2. 把該函式綁在B上

    綁在Sister上的allocateMoney(argue)

-----------------【開始】-----------------
(按下Sister按鍵)

  1. 在B元件呼叫綁定在props上的A元件函式

    Sister呼叫argue(allocateMoney)

  2. A元件的函式修改了綁在C上的state,C元件的props就會被改變

    Parent的allocateMoney改變了Money,綁在Brother上的Money被改變

  3. C元件的偵測到props被改變,用C資料作為參數呼叫對應C元件函式

    Brother偵測到Money改變,以expectAmount為參數呼叫argue(透過setMyFeeling間接呼叫)

  4. A元件綁在B元件props上的state就會被改變, B元件透過props取得資料

    A元件的moneyForBrother,也就是綁在Sister上的moneyBrother被改變為Brother的expectAmount

-----------------【結束】-----------------

邏輯圖(順序由深至淺):

Part.4 - 祖先對某代孫子

假設今天我們要在某個父元件直接傳值、讓他的孫子直接使用他綁的props,像是這個架構:

Parent.js
|____ Brother.js
       |____GrandSon.js

在這個狀況下,階層中間的元件(ex: Brother.js)只是一個中繼站的感覺。就變成是多層父子溝通。但如果要傳的東西很多,每一層都要綁this.props.名稱會有點麻煩。

Part. 4-1 - 有沒有辦法能把Parent給Brother所有的props一次綁給GrandSon呢?

有的,要運用【React.js入門 - 03】 開始之前應該要知道的DOM和ES6講過的spread operator:

/* Brother.js */

<GrandSon {...this.props}/>

利用上方的寫法,就能把Brother所有的props全部綁在GrandSon上,且在GrandSon使用這些props的方法完全一模一樣。 這樣就能避免在多階層溝通中,因為綁很多props讓可讀性大幅降低。

Part. 4-2 - 有沒有不用綁全部props、又更簡易的方法?(不用其他插件)

有的,要運用Bracket notation(以中括號存取物件)。

也就是在作為第一個中繼站的父元素中,定義兩個函式(在這個例子是Brother.js):

  1. 存取資料用: 接收一個名字參數,然後把這個名字丟到Bracket notation中,去存取想要的props資料並回傳。
    handleSendData(name){
        return this.props[name];
    }
    
  2. 存取函式用: 接收兩個參數,第一個參數是用來接收要呼叫的函式名稱,第二個參數用spread operator去把剩餘所有的參數複製成一個array。然後把函式名稱丟到Bracket notation中,去呼叫想要的函式,並再次利用spread operator把剛剛複製的array展開丟到參數內(也就是剛剛複製的array就是真正要丟到呼叫函式的參數),最後回傳函式return值。
    handleSendFunc(method, ...arg){
        return this.props[method](...arg);
    }
    

然後把這兩個函式綁在夾在目標子元素跟第一個中繼站之間所有中繼站上(目標子元素也要綁),在這個案例中繼站只有一個,所以只要綁目標子元素。

<GrandSon handleSendData={this.handleSendData} handleSendFunc={this.handleSendFunc} />

最後,我們只要在目標子元素呼叫這兩個函式,並把要存取/呼叫的東西以字串丟入函式參數,就能拿到/呼叫想要的東西:

/* 存取綁在Brother.js上的money */
let dadMoney=this.handleSendData("money"); 

/* 呼叫綁在Brother上的argue函式(allocateMoney)。
「brother」和「5」是原本allocateMoney規定需要的參數。 */
this.handleSendFunc("argue","brother",5); 

這樣就不用把全部props都綁在每一層

由於祖先對某代孫子的溝通的情形很常發生,因此後來衍生了產生Global state的官方或第三方插件,後面會提一下他們是甚麼,但本系列不會特別去講使用方法。

2020/05/07
我補寫了Context api的相關筆記,有關多層子父元素溝通可參考:
【React.js 筆記】- 使用useContext和useReducer進行多層子父元件溝通:

小結:

這一篇是我認為新手學習現代框架最重要的觀念,對物件導向很熟悉的人來說應該會容易理解。如果能夠熟悉從props到這一篇為止的操作,那麼再去學習其他框架時也能很快的上手。


上一篇
【React.js入門 - 20】 useEffect - 在function component用生命週期
下一篇
【React.js入門 - 22】 元件練習(上) - 在class利用遞迴+state實作動畫
系列文
給初入JS框架新手的React.js入門31

尚未有邦友留言

立即登入留言