React (3) 元件建立

建立元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Card = ({ title, price, content, callToAction }) => {
return (
<div className="card mb-4">
<div className="card-header">
<h4 className="my-0">{title}</h4>
</div>
<div className="card-body">
<h1 className="card-title">
{ typeof price === 'number' ? `$${price}`: price }
</h1>
<p>{content}</p>
<button type="button" className="btn btn-outline-primary">
{callToAction}
</button>
</div>
</div>
)
}

上面是一個卡片元件,React 元件必須以大寫開頭命名。卡片的內的資料可以透過 props 傳入,上面是透過解構的方式傳入 props 。如果不透過解構則會如下面方式取得資料。

1
2
3
const Card = (props) => {
{ props.title }
}
  • return 原則
  1. 結構只有一行時,可以直接 return
1
return <img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />;
  1. 結構超過一行,必須加上 ()
1
2
3
4
5
return  (
<div>
<img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />
</div>
);

父層傳入資料

在父層可透過資料驅動的方式,將資料代入子層 Card 元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const App = () => {
const products = [
{
id: 1,
title: '基本版',
price: 1000,
content: '基本每週配給,包含高麗菜、空心菜,健康不馬乎',
callToAction: '免費試用',
},
{
id: 2,
title: '進階版',
price: 6000,
content: '奶蛋魚肉經營養師精確估計,適合三人小家庭使用,每週一、四配送',
callToAction: '永保健康',
},
{
id: 3,
title: '企業版',
price: '請洽客服',
content: '多少人數均可訂製,5 ~ 1000 人團體均適用',
callToAction: '大吃大喝方案',
},
];
return (
<>
<h2>月月送菜到你家</h2>
<p>Lorem ipsum dolor sit amet</p>
{
products.map((product) => {
return <Card key={product.id} title={product.title} price={product.price} content={product.content}
callToAction={product.callToAction}/>
})
}
</>
)
}

上面將 products 陣列資料透過 map 的方式一一渲染到畫面,並將每筆 product 資料帶入 Card 元件內。

使用 key 值

當渲染陣列資料時,要在每個元件代入 key 值,並且 key 值要是唯一值。

  1. 提升渲染效能: 當你渲染一組列表時,key 幫助 React 快速判斷哪些元素需要更新、刪除或重新排序。這比每次重新渲染整個列表要高效得多。例如,如果你有一個包含多個項目的待辦事項列表(To-Do List),當你新增一個項目時,React 會利用 key 來只更新新增的部分,而不是重新渲染整個列表。

  2. 保持元件狀態的一致性: 唯一的 key 可以確保元件在更新時保持狀態的一致性。舉個例子,假設你有一個可以打勾完成的待辦事項列表,如果每個項目沒有唯一的 key,當你刪除其中一個項目時,React 可能會誤解為其他項目發生了變動,導致狀態錯亂。

  3. 避免潛在的錯誤: 如果沒有提供唯一的 key,或是 key 不是唯一的,React 會發出警告,提醒你可能會導致渲染問題或效能問題。這時候,你可能會遇到列表項目顯示不正確、狀態錯誤等問題。

迴圈一次帶入多個 dom

使用 map 將資料渲染到畫面時,若 dom 結構不只一個,可以用 <Fragment></Fragment> 包起來

1
2
3
4
5
6
7
8
import { Fragment } from 'react';

const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);

props 預設值

元件接收父層傳入的 props 時,也可設定預設值:

  1. 當父層沒有傳入值,或是傳入的是 size={ undefined } 時使用預設值。
  2. 傳入的是 size={null} or size={0},不會使用預設值
1
2
3
function Avatar({ person, size = 100 }) {
// size 沒有傳入時,使用預設值 100
}

不要在 state 中鏡像(mirror)props

初學者經常會將從 props 傳來的資料複製到 state 中,這樣看似可以讓組件本地化管理資料,但實際上這樣會導致資料不一致。

錯誤範例:

1
2
3
4
5
6
7
8
9
function UserProfile({ name }) {
const [userName, setUserName] = useState(name);

useEffect(() => {
setUserName(name); // 嘗試同步 state 與 props
}, [name]);

return <h1>{userName}</h1>;
}
  • 重複資料:name 是從 props 傳進來的,但它又被複製到了 state 中(userName)。這會導致兩個地方存放同樣的資料。

  • 同步問題:當 props name 更新時,我們需要手動調用 setUserName(name) 來更新 state,這會增加開發負擔並且很容易出錯。如果忘記或未能正確更新 state,UI 可能顯示過時的資料。

正確做法

1
2
3
function UserProfile({ name }) {
return <h1>{name}</h1>; // 直接使用 props
}
  • 保持單一資料來源:name 是 props 傳進來的,React 會自動重新渲染組件,只要 name 發生變化,這樣可以確保 UI 與資料保持同步。

  • 減少不必要的 state:這樣做簡化了邏輯,因為不需要再手動同步 props 與 state,減少了錯誤的風險。

巢狀元件

元件本身也可以帶入其他元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const PrimaryButton = ({ className, callToAction }) => {
return <button type="button" className={`btn ${className}`}>{callToAction}</button>
}
const Card = ({title, price, content, callToAction}) => {
return (<div className="card mb-4">
<div className="card-header">
<h4 className="my-0">{title}</h4>
</div>
<div className="card-body">
<h1 className="card-title">{typeof price === 'number' ? <> <small>${price} / 月</small> </> : price }</h1>
<p>{content}</p>
{
title === '企業版' ?
<PrimaryButton className="btn-primary" callToAction={callToAction}/> :
<PrimaryButton className="btn-outline-primary" callToAction={callToAction}/>
}
</div>
</div>)
}

上面 Card 元件內帶入了 PrimaryButton 的元件。

Children props

除了用 props 將資料帶入元件,也可以用 children 將整個 html 結構代入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const PrimaryButton = ({ className, children }) => {
return <button type="button" className={`btn ${className}`}>
{children}
</button>
}

const Card = ({title, price, content, callToAction}) => {
return (<div className="card mb-4">
<div className="card-header">
<h4 className="my-0">{title}</h4>
</div>
<div className="card-body">
<h1 className="card-title">{typeof price === 'number' ? <> <small>${price} / 月</small> </> : price }</h1>
<p>{content}</p>
{
title === '企業版' ?
<PrimaryButton className="btn-primary">
<i className="bi bi-gem"></i> { callToAction }
</PrimaryButton> :
<PrimaryButton className="btn-outline-primary">
{ callToAction }
</PrimaryButton>
}
</div>
</div>)
}

在 PrimaryButton 元件內加入 html 結構,並用 children 將結構代入元件內。

pops 更新時,自動更新元件內所有狀態

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}

function Profile({ userId }) {
// ✅ This and any other state below will reset on key change automatically
const [comment, setComment] = useState('');
// ...
}

在 React 中,key 屬性用於識別哪些組件需要重新渲染或更新。它為每個組件實例提供了一個唯一的標識。當 userId 改變時,因為 key 也隨之改變,React 會重建 Profile 元件,從而重置元件內的所有狀態(如 comment)。

如果不加 key,當 userId 改變時,會發生以下情況:

React 會認為這是同一個 Profile 組件實例,因此不會重新創建該組件。結果是:

  • 狀態不會重置:組件內的狀態(如 useState 的 comment)將保持不變。
  • 副作用不會重新執行:如果有使用 useEffect 的副作用,也不會因為 userId 改變而重新執行,除非 useEffect 的依賴陣列包含 userId。