感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。
昨天的網速單位轉換器仍然還是半成品,因為 <CardFooter />
組件的樣式雖然可以透過我們手動修改 inputValue
的值而有變化,但是它還沒辦法根據使用者輸入的內容,自動變換這樣些樣式。今天就讓我們來看一下要怎麼在組件之間傳遞和修改資料。
以下是今天會涵蓋到的內容:
開始前你可以先打開昨天完成的 CodePen,或從 Day 10 - Network Speed Converter with multiple components 複製一份出來。
從下圖中可以看到,inputValue
可以取得使用者在對話框中輸入的內容,但 inputValue
是存在於 <SpeedConverter />
這個父層組件的資料狀態,現在為了要讓 <CardFooter />
可以根據 inputValue
的值而改變樣式,我們必須要把 inputValue
的值傳到 <CardFooter />
:
在 React 中把父層組件的資料狀態傳遞到子層組件的方式非常簡單,只需要透過類似 HTML 屬性的方式放在該組件的標籤內就可以了,接著在子層組件的參數中,就可以透過 props
把傳入的資料取出,像是這樣:
// STEP 2: 在該 component 內可以透過參數 props 取得傳入的資料
function ChildComponent(props) {
return <h1>Hello, {props.firstName} {props.lastName}</h1>; // Hello, Aaron Chen
}
// STEP 1: 將資料透過 html 屬性的方式傳入 component 內
const element = <ChildComponent firstName="Aaron" lastName="Chen" />;
在取用 props 的時候,會習慣使用解構賦值直接把需要的變數取出來,因此在取用 props
的地方會像這樣寫:
// 透過解構賦值把 props 內需要用到的變數取出
function ChildComponent(props) {
const { firstName, lastName } = props;
return <h1>Hello, {firstName} {lastName}</h1>; // Hello, Aaron Chen
}
甚至更精簡到連 props
都不命名了,直接取出來用:
// 透過解構賦值直接在「函式參數的地方」把需要用到的變數取出
function ChildComponent({ firstName, lastName }) {
return <h1>Hello, {firstName} {lastName}</h1>; // Hello, Aaron Chen
}
因此在網速轉換器的這個範例中,我們可以在 <SpeedConverter />
組件中使用 <CardFooter />
的地方,把想要傳入的資料透過 <CardFooter key={value} />
的方式傳入:
const SpeedConverter = () => {
const [inputValue, setInputValue] = useState(0);
const handleInputChange = (e) => {
const { value } = e.target;
setInputValue(value);
};
return (
<div className="container">
{/* ... */}
{/* STEP 1: 把想要傳入 CardFooter 的資料透過 key={value} 的方式傳入 */}
<CardFooter inputValue={inputValue} />
</div>
);
};
提示:在使用 props 傳遞資料時,
key
和value
的命名可以自己取,不需要相同,只是這裡都剛好取做inputValue
。
接著在 <CardFooter />
的組件中,就可以在參數中透過 props
取得傳進來的資料,props
本身會是一個物件,因此一樣可以透過解構賦值的方式,把想要的資料取出:
// STEP 2:透過 props 取得從父層傳入的資料
const CardFooter = (props) => {
const { inputValue } = props;
// ...
return (
{/* ... */ }
);
};
整個流程會像這樣:
如果你對於 props
還不是這麼熟悉的話,也可以在 <CardFooter />
中透過 console.log(props)
把它呈現出來看一下。
如此,將可以把使用者在對話框中輸入的內容傳入 <CardFooter />
內,而 <CardFooter />
取得 inputValue
後,就可以根據它的值來決定要呈現的樣式:
完整的程式碼可以參考 Day 11 - Network Speed Converter with props。
為了更熟悉資料傳遞的這個概念,我們來練習把先前完成的計數器加上起始值。還記得我們可以透過使用很多次的 <Counter />
來重複渲染很多個計數器嗎?現在我們希望畫面上有五個計數器,但每個計數器一開始都有不一樣的起始值,分別就是 1 ~ 5,像下圖這樣,可以怎麼做呢?
你可以從這個 Day 8 - Multiple Counters Finished CodePen 開始,想想看可以怎麼做!
首先,我們可以在每一次渲染 <Counter />
的地方透過 props
帶入起始值。這裡我先把原本用 map
自動產生多個 <Counter />
的部分註解掉,手動去寫:
ReactDOM.render(
<div
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
{/* counters.map(item => <Counter />) */}
{/* STEP 1:把每一個 <Counter /> 中透過 `startingValue={}` 的方式給入起始值 */}
<Counter startingValue={1} />
<Counter startingValue={2} />
<Counter startingValue={3} />
<Counter startingValue={4} />
<Counter startingValue={5} />
</div>,
document.getElementById('root')
);
接下來在每一個 <Counter />
組件內一樣可以透過 props
的方式取得 startingValue
,在把這個值放在 useState(<defaultValue>)
中,如此即會變成 count
的預設值,像是這樣:
// STEP 2:透過 props 取得外層傳入的 startingValue
const Counter = (props) => {
// STEP 3: 使用解構賦值把 startingValue 從 props 中取出
const { startingValue } = props;
// STEP 4: 把 startingValue 當作 count 的預設值放入 useState 的參數中
const [count, setCount] = useState(startingValue);
const handleIncrement = () => setCount(count + 1);
const handleDecrement = () => setCount(count - 1);
return (
{/* ... */}
);
};
如此就可以讓每個 <Counter />
在一開始的時候都帶有不同的起始值。完整的程式碼可以參考 Day 11 - Multiple Counters with different startingValue by props。
現在如果我們想要讓這 5 個 <Counter />
也都有各自不同的最大值和最小值的話,你可以想想看該怎麼做嗎?完成的效果會像這樣:
實際的做法和上面幾乎雷同,因此就不再贅述,你可以花 5 ~ 10 分鐘自己嘗試看看,完成的程式碼可以參考 Day 11 - Multiple Counters with different min and max number by props @ CodePen。
慣例上所有 React Hooks 的方法都會以 use
作為函式名稱的開頭,例如,useState
、useEffect
、useCallback
、...等等。現在雖然我們只提到了 useState
,但在使用 React Hooks 的方法時有些原則一定要注意。
其中最重要的一個原則是:「不能在條件式(conditions)、迴圈(loops)或嵌套函式(nested functions)中呼叫 Hook 方法」。
什麼意思呢?以 useState
來說,這樣的寫法是正確的:
// ✅ 正確使用
const Counter = () => {
const [count, setCount] = useState();
return (
{/* ... */}
);
};
但如果因為某些原因而把 useState
放到 if
內時可能會導致嚴重錯誤:
// ❌ 錯誤使用,把 React Hooks 放到 if 內
const Counter = () => {
if (isValidCounter <= 10) {
const [count, setCount] = useState();
}
return {
/* ... */
};
};
要留意的是,以 use
開頭的函式不能放在判斷式內,但像這裡透過 useState()
產出的變數(count
)和方法 setCount
,則是可以在判斷式內使用的。
之所以會有這樣的規定是因 React 組件(例如,<Counter />
)每次在渲染或更新畫面時,都會呼叫產生這個組件的函式(Counter()
),而在 React Hooks 中會去記錄這些 Hooks 在函式中被呼叫的順序,以確保資料能夠被相互對應,但若當我們將 Hooks 放到條件式或迴圈時,就會破壞了這些 Hooks 被呼叫到的順序,如此會造成錯誤。
SpeedConverter的handleInputChange,在setInputValue
的時候因為e.target.value
是字串,所以後面inputValue的型別就會被改為字串而不是數字,於是會有個效果是,當inputValue的值更新過後又再次更新成0的時候,CardFooter會顯示SLOW
而不是初始的---
所以如果希望網速回到0Mbps的時候當作初始化顯示---
,可以加上parseInt(value)
:
const handleInputChange = (e) => {
const { value } = e.target;
setInputValue(parseInt(value));
}