iT邦幫忙

2022 iThome 鐵人賽

DAY 12
1
Modern Web

一次打破 React 常見的學習門檻與觀念誤解系列 第 12

[Day 12] 如何在子 component 裡觸發更新父 component 的資料

  • 分享至 

  • xImage
  •  

React 中以 state 資料以及 setState 作為 reconciliation 的觸發點,並且以 props 作為 component 層層往下的資料傳遞媒介。很多剛學習 React 的新手在學習拆分 component 時都會遇到一個問題,就是不知道「要怎麼在子 component 裡觸發更新父 component 的資料」。因此我們接下來就稍微探討一下該如何以 setState 與 props 交互配合,來讓 component 分拆的情況下讓我們的應用程式順利觸發資料與畫面的更新。


Props 是唯讀且不可變的

首先,component 的 props 是一種「唯讀且不可變的資料」,一旦由 component 的外部傳入後,就絕對不可以直接在內部修改其內容

參考以下的範例:

function App(props) {
  // 注意,不應該在 component 內進行以下的 props 修改操作
  props.a = "hello";
  props.b = "world";
  props.c = "new prop";

  return (
    <ul>
      <li>prop a: {props.a}</li>
      <li>prop b: {props.b}</li>
    </ul>
  );
}

// 直接在參數定義處解構 props 的情況
function App({ a, b }) {
  // 注意,不應該在 component 內進行以下的 props 修改操作
  a = "hello";
  b = "world";

  return (
    <ul>
      <li>prop a: {a}</li>
      <li>prop b: {b}</li>
    </ul>
  );
}

以上範例中, App component function 內對於傳入的 props 物件的任何修改都是不應該且不允許的,這樣做有可能會導致意外的問題。並且這個規定不只是 React 實作設計上的刻意限制,也是一種 「讓 props 保證永遠是從外部傳入的原樣」的 pattern,進而讓 component 內更容易追蹤資料的來源,對於維持單向資料流的可靠性以及提升程式碼的可維護性都相當重要。

如果你需要以 props 做資料的延伸計算的話,那應該要將計算的結果存到另外新宣告的變數中,而不是直接修改原有的 props 物件或變數。


setState 會觸發 re-render 的 component 是固定的

當一個 component state 的 setState 方法被傳遞到其他 component 並被呼叫時,仍然是該 state 原本所屬的 component 會被 re-render:

import { useState } from 'react';

function IncrementButton({ onClick }) {
  console.log('render IncrementButton');
  return <button onClick={onClick}>+</button>;
}

export default function App() {
  console.log('render App');
  const [count, setCount] = useState(0);

  const decrement = () => {
    setCount(previousCount => previousCount - 1);
  };

  const increment = () => {
    setCount(previousCount => previousCount + 1);
  };

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <IncrementButton onClick={increment} />
    </div>
  );
}

Apr-17-2022 15-36-24.gif

上面的範例中可以看到,我們在 App 中定義了 count state,並且將呼叫了 setCountincrement() 方法透過 props 傳進了子 component IncrementButton,而 IncrementButton 裡我們將這個從 props 傳進來的方法綁定在 buttononClick 上。

當我們點擊 IncrementButton 時,你會看到雖然 setCount 是在 IncrementButton 裡被執行的,但是從 console 先印出的 render App 就可以看出,這次的 reconciliation 仍是從 App 發起 re-render 的,然後才連帶觸發 IncrementButton 的 re-render。


從子 component 觸發更新父 component 的 state 資料

總合以上我們對於 props 以及 state 的了解,你會發現 React 的資料流確實是嚴格遵守單向資料流的概念的。當我們將父 component 中的 state 以 props 的形式傳到子 component 中的時候,我們在子 component 中當然無法直接修改這個來自父 component 的資料:

import { useState } from 'react';

function Parent() {
  const [name, setName] = useState('Zet');
  return (
    <>
      <h1>Render name in Parent: {name}</h1>
      <Child name={name} />
    </>
  );
}

function Child(props) {
  const repeatName = () => {
    props.name = props.name.repeat(2); // ❌ 不合法的操作,不可修改 props
  };
	
  return (
    <>
      <h2>Render name in Child: {props.name}</h2>
      <button onClick={repeatName}>
        repeat the name string
      </button>
    </>
  );
}

然而除了 state 資料本身可以當作 props 傳遞之外,state 資料所對應的 setState 函式其實也可以作為 props 來傳遞給子 component,並且在子 component 中去呼叫它:

import { useState } from 'react';

function Parent() {
  const [name, setName] = useState('Zet');
  return (
    <>
      <h1>Render name in Parent: {name}</h1>
      <Child name={name} setName={setName} />
    </>
  );
}

function Child(props) {
  const repeatName = () => {
    props.setName(prevName => prevName.repeat(2));
  };

  return (
    <>
      <h2>Render name in Child: {props.name}</h2>
        <button onClick={repeatName}>
          repeat the name string
        </button>
    </>
  );
}

在上述範例中,當我們在 Child 裡呼叫從 Parent 傳遞進來的 setName 函式後,會看到 Child 自己的 props.name 也隨之更新了。這是因為呼叫 setName 會導致自動修改 props.name 的值嗎?其實並不是,而是因為前文有提到的行為「setState 會觸發 re-render 的 component 是固定的」,因此無論你將 setName 方法從 Parent 傳遞到任何一個地方並執行,它都固定只會觸發原來的 Parent 的 re-render:

https://i.imgur.com/RZfovmG.png

  1. initial render 時, Parentname state 的預設值以及其對應的 setName 函式以 props 的形式連帶傳給 Child 並 render
  2. Child 中的事件處理呼叫了從 props 傳下來的函式 setName(newName)
  3. setName 原本是對應定義在 Parent 中的 state,因此會觸發 Parent 的 re-render
  4. Parent 重新 render 時從 name state 資料取出更新後的值 ,而由於 render 的內容中含有 Child,因此也會以新的 name 資料來作為 props 傳遞給 Child 並觸發其連帶 re-render
  5. Child 被父 component 連帶觸發 re-render,接收到新的 name prop 並以該資料來更新畫面結果

總結整理

總結來說,當你想從子 component 去觸發更新父 component 的資料時,必須要由父 component 本身就有以 props 傳遞 setState(或是 useReducerdispatch)下來才可以。這個流程能很好地體現了 React 的單向資料流的 pattern:

  • 子 component 無法直接修改接收到的 props 來達到修改畫面結果的目的,更不能直接去修改父 component 的資料
  • 但是如果父 component 有將更新 state 資料的 setState 函式作為 prop 傳遞下來的話,子 component 就可以透過「呼叫傳遞下來的函式」的方式來觸發父 component 的資料與畫面更新
  • 然後如果該父 component 的 state 值也會以 props 的形式傳遞下來子 component 的話,子 component 就會因為連帶的 re-render 也接收到新版的資料並更新畫面

2024/2 更新 - 實體書平裝版本預購

在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~

《React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者》

目前首刷的軟精裝版本各大通路已經幾乎都銷售一空,接下來會再刷推出新的平裝版本:

天瓏(平裝版預購):
https://www.tenlong.com.tw/products/9786263337695

博客來(平裝版):
https://www.books.com.tw/products/0010982322

momo(平裝版):
https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=12528845


上一篇
[Day 11] React 畫面更新的核心機制(下):Reconciliation
下一篇
[Day 13] 深入理解 batch update
系列文
一次打破 React 常見的學習門檻與觀念誤解30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言