(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
在使用component時,我們常會遇到不同元件需要互相傳資料、呼叫函式的情況,大致可分為:
自己注意到身旁剛接觸js框架或是對於物件導向階層不熟的朋友,在操作各層元件的溝通時比較難快速上手,所以特地整理了這篇。
我想把所有溝通都整理在一起,所以這一篇很長、超長,要有心理準備QQ
我們將會在這樣的javasript的程式階層下講解:
index.js
    |____ App.js
          |____ Parent.js
                |____ Sister.js
                |____ Brother.js
並以class component作為講解的架構。
在某個地方,有一對很開明的父母,育有一對子女。只要子女有求他們必應。
有一天孩子長大了,他們決定以總金額100分配給他們兩個零用錢,不過對兒子和女兒兩個人而言,他們各自期望著不同的金額。一開始,他們讓兒子分到60塊。女兒分到40塊,並且先問了女兒的意見。
(寫完的時候才發現命名用daughter和son好像比較好哈哈,不過圖都畫了就算了。)
在接下來的說明中,【開始】是表示目前的步驟開始實現標題所指定的行為。不是代表說程式碼寫了【開始】之前的東西後面的東西就會自動跑。
就是props,不解釋
子元素不能直接修改props的值(read-only),下面這樣的函式就是錯的:
this.props.money= 70;
如果要修改props,只能照以下步驟:
-----------------【準備】-----------------
-----------------【開始】-----------------
-----------------【結束】-----------------
在子元件傳資料給父元件/透過父元件函式更改父元件也是用同樣的方法。
例如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塊)
邏輯圖(順序由深至淺):

父元件是沒有辦法直接觸發子元件的函式的,必須透過以下步驟
-----------------【準備】-----------------
-----------------【開始】-----------------
-----------------【結束】-----------------
例如如果今天兒子的情緒表現是一個函式(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();的迴圈。
邏輯圖(順序由深至淺):

一樣的,父元素沒辦法直接主動取得子元素中的資料。但我們可以結合前面:
在父元件主動取得子元素的資料 =  在父元件主動觸發子元件的函式 + 在子元件主動傳資料給父元件
也就是:
-----------------【準備】-----------------
allocateMoney)money)-----------------【開始】-----------------
money)expectAmount)傳入該函式參數-----------------【結束】-----------------
例如,我們先綁定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);
    }
就等同於
allocateMoney()
Money
Money被改變,以expectAmount為參數來呼叫argue()(透過setMyFeeling()間接呼叫)argue()的allocateMoney()取得Brother的expectAmount
也就是我們用Parent主動取得了Brother期望的錢。
邏輯圖(順序由深至淺):

實際上剛剛的過程跑完,我們就已經完成同階層子對子的溝通了。
我們來解釋一下:
-----------------【準備】-----------------
綁在Brother上的
Money& Parent的allocateMoney
綁在Sister上的
allocateMoney(argue)
-----------------【開始】-----------------
(按下Sister按鍵)
Sister呼叫
argue(allocateMoney)
Parent的
allocateMoney改變了Money,綁在Brother上的Money被改變
-----------------【結束】-----------------
邏輯圖(順序由深至淺):

-----------------【準備】-----------------
綁在Brother上的
Money& Parent的allocateMoney
綁在Sister上的
allocateMoney(argue)
-----------------【開始】-----------------
(按下Sister按鍵)
Sister呼叫
argue(allocateMoney)
Parent的
allocateMoney改變了Money,綁在Brother上的Money被改變
Brother偵測到
Money改變,呼叫setMyFeeling()
-----------------【結束】-----------------
邏輯圖(順序由深至淺):

這裡,我們只要把兒子的錢也綁在女兒上就完成了:
<Sister money={this.state.moneyForSister} moneyBrother={this.state.moneyForBrother} argue={this.allocateMoney}/>
-----------------【準備】-----------------
綁在Sister上的
MoneyBrother& 綁在Brother上的Money& Parent的allocateMoney
(這個例子中,此函式兩個state都能設定)
綁在Sister上的
allocateMoney(argue)
-----------------【開始】-----------------
(按下Sister按鍵)
Sister呼叫
argue(allocateMoney)
Parent的
allocateMoney改變了Money,綁在Brother上的Money被改變
Brother偵測到
Money改變,以expectAmount為參數呼叫argue(透過setMyFeeling間接呼叫)
A元件的
moneyForBrother,也就是綁在Sister上的moneyBrother被改變為Brother的expectAmount
-----------------【結束】-----------------
邏輯圖(順序由深至淺):
假設今天我們要在某個父元件直接傳值、讓他的孫子直接使用他綁的props,像是這個架構:
Parent.js
|____ Brother.js
       |____GrandSon.js
在這個狀況下,階層中間的元件(ex: Brother.js)只是一個中繼站的感覺。就變成是多層父子溝通。但如果要傳的東西很多,每一層都要綁this.props.名稱會有點麻煩。
有的,要運用【React.js入門 - 03】 開始之前應該要知道的DOM和ES6講過的spread operator:
/* Brother.js */
<GrandSon {...this.props}/>
利用上方的寫法,就能把Brother所有的props全部綁在GrandSon上,且在GrandSon使用這些props的方法完全一模一樣。 這樣就能避免在多階層溝通中,因為綁很多props讓可讀性大幅降低。
有的,要運用Bracket notation(以中括號存取物件)。
也就是在作為第一個中繼站的父元素中,定義兩個函式(在這個例子是Brother.js):
handleSendData(name){
    return this.props[name];
}
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到這一篇為止的操作,那麼再去學習其他框架時也能很快的上手。