React 中以 state 資料以及 setState 作為 reconciliation 的觸發點,並且以 props 作為 component 層層往下的資料傳遞媒介。很多剛學習 React 的新手在學習拆分 component 時都會遇到一個問題,就是不知道「要怎麼在子 component 裡觸發更新父 component 的資料」。因此我們接下來就稍微探討一下該如何以 setState 與 props 交互配合,來讓 component 分拆的情況下讓我們的應用程式順利觸發資料與畫面的更新。
首先,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>
);
}
在上面的範例中可以看到,我們在 App
中定義了 count
state,並且將呼叫了 setCount
的 increment()
方法透過 props 傳進了子 component IncrementButton
,而 IncrementButton
裡我們將這個從 props 傳進來的方法綁定在 button
的 onClick
上。
當我們點擊 IncrementButton
時,你會看到雖然 setCount
是在 IncrementButton
裡被執行的,但是從 console 先印出的 render App
就可以看出,這次的 reconciliation 仍是從 App
發起 re-render 的,然後才連帶觸發 IncrementButton
的 re-render。
總合以上我們對於 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:
Parent
將 name
state 的預設值以及其對應的 setName
函式以 props 的形式連帶傳給 Child
並 renderChild
中的事件處理呼叫了從 props 傳下來的函式 setName(newName)
setName
原本是對應定義在 Parent
中的 state,因此會觸發 Parent
的 re-renderParent
重新 render 時從 name
state 資料取出更新後的值 ,而由於 render 的內容中含有 Child
,因此也會以新的 name
資料來作為 props 傳遞給 Child
並觸發其連帶 re-renderChild
被父 component 連帶觸發 re-render,接收到新的 name
prop 並以該資料來更新畫面結果總結來說,當你想從子 component 去觸發更新父 component 的資料時,必須要由父 component 本身就有以 props 傳遞 setState
(或是 useReducer
的 dispatch
)下來才可以。這個流程能很好地體現了 React 的單向資料流的 pattern:
setState
函式作為 prop 傳遞下來的話,子 component 就可以透過「呼叫傳遞下來的函式」的方式來觸發父 component 的資料與畫面更新在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~
《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