0%

React State in Component Trees

組件樹中的狀態

在每個組件中使用狀態不是一個好主意。 將狀態數據分佈在過多的組件中,將使追蹤錯誤和在應用程序中進行更改變得更加困難。發生這種情況,是因為很難追踪狀態值在組件樹中的位置。如果您統一從單一的位置進行狀態管理,將更容易理解應用程序的狀態。

在 React 有多種方法可達到此目的,這裡將分析的第一個方法是將狀態存儲在組件樹的根組件中,然後通過屬性(props)將其傳遞給子組件。

我們要構建一個 “顏色管理器”,用於保存顏色列表的小型應用程序,它將允許用戶管理顏色列表、自定義標題並使用先前建立的 StarRating 組件評定顏色等級。

首先,樣本數據如下所示。可以將它放在 APEX Page Properties 的 JavaScript > Function and Global Variable Declaration 中。

Demo module
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
var Demo = ((Demo) => {
const uuid = () => {
const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
};

const colorData = [
{
id: uuid(),
title: "ocean at dust",
color: "#00c4e2",
rating: 5
},
{
id: uuid(),
title: "lawn",
color: "#26ac56",
rating: 3
},
{
id: uuid(),
title: "bright red",
color: "#ff0000",
rating: 0
},
];

return { uuid, colorData, ...Demo };
})(Demo || {});

我們將創建一個由 React 組件組成的 UI,來顯示這些數據。也將允許用戶添加新顏色以及對顏色進行等級評定和刪除顏色。

由根組件向下發送狀態

我們將狀態存儲在 App 根組件中,並將顏色向下傳遞給子組件以處理渲染。App 組件將是我們應用程序中唯一擁有狀態的組件。我們將使用 useState 掛鉤將顏色列表添加到 App 中:

Color Organizer
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
<div id="react-container"></div>

<script type="text/babel">
const { useState } = React;
const { render } = ReactDOM;
const { colorData } = Demo;

const Color = ({ id, title, color, rating }) => {
return (
<section>
<h1>{title}</h1>
<div style={{ height: 50, backgroundColor: color }} />
</section>
);
};

const ColorList = ({ colors = [] }) => {
if(!colors.length) return <div>No Colors Listed.</div>;

return (
<div>
{
colors.map(color => <Color key={color.id} {...color} />)
}
</div>
);
};

const App = () => {
const [colors] = useState(colorData);

return <ColorList colors={colors} />
};


render(<App />, document.getElementById("react-container"));
</script>

App 組件位於樹的根部,第 30 行在該組件添加 useState 使其與 colors 的狀態管理掛鉤。在第 32 行並將顏色數據通過屬性傳遞給 ColorList 組件。並依序在第 23 行將每種顏色的詳細信息傳遞到樹的更遠端 Color 組件。

我們允許對顏色進行等級評定,所以要將先前建立的 StarRating 組件加到 Color 組件,這將允許每個 Color 組件擁有自己的 StarRating 子組件。並將顏色數據中的 rating 沿著樹向下傳遞到 StarRating 的子組件,以顯示評定的星星個數。

Color
1
2
3
4
5
6
7
8
9
const Color = ({ id, title, color, rating }) => {
return (
<section>
<h1>{title}</h1>
<div style={{ height: 50, backgroundColor: color}} />
<StarRating selectedStars={rating} />
</section>
);
};

先前的 StarRating 組件將選擇的星星個數 selectedStars 儲存在自己組件的狀態中,我們必須修改 StarRating 組件將其變成純組件(pure component)。純組件是不包含狀態(state)的函式組件(function component)。

我們將此組件設為純組件,因為顏色評級的數據 rating 將存儲在組件樹 App 根組件的 colors 數據中,而不再儲存在自己組件的狀態中。

以下是修改前原先的 StarRating 組件,是一個包含狀態的組件。

StarRating
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const StarRating = ({ totalStars = 5 }) => {
const [selectedStars, setSelectedStars] = useState(0);

return (
<>
{createArray(totalStars).map((n, i) => (
<Star
key={i}
selected={selectedStars > i}
onSelect={() => setSelectedStars(i + 1)}
/>
))}
<p style={{fontSize: "1.5em", margin: "10px"}}>{selectedStars} of {totalStars} stars</p>
</>
);
};

我們將拿掉第 2 行使用 useState 狀態管理的 selectedStars 變數、setSelectedStars 函式,與第 10 行 setSelectedStars 的調用。

StarRating
1
2
3
4
5
6
7
8
9
10
11
12
13
const StarRating = ({ totalStars = 5, selectedStars = 0 }) => {    
return (
<>
{createArray(totalStars).map((n, i) => (
<Star
key={i}
selected={selectedStars > i}
/>
))}
<p style={{fontSize: "1.5em", margin: "10px"}}>{selectedStars} of {totalStars} stars</p>
</>
);
};

現在在第 1 行 selectedStars 直接由父組件 Color 通過屬性傳入,這將會是顏色數據中的 rating。 這裡也暫時拿掉 onSelect 屬性,稍後再來處理。

現在應用程序執行後看起來會像是:

至此,我們已經完成了將狀態從 App 組件一直向下傳遞到 FaStar 組件的過程。

將互動由葉端向上傳送到根組件

到目前為止,我們透過 props 屬性將數據從父組件向下傳遞到子組件。現在,如果我們要從列表中刪除顏色或更改列表中的顏色評等等級,會發生什麼?

顏色數據以狀態存儲在樹的根組件 APP,但我們需要從子組件中收集使用者的互動,並將其沿著樹狀結構發送回根組件,以便在根組件中更改狀態。

Remove Color

例如,假設我們要在每種顏色的標題下端添加一個刪除按鈕,以允許用戶從狀態中刪除顏色。我們將該按鈕添加到 Color 組件中:

Color component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const FaRemove = () =>  <span className="fa fa-remove"></span>;

const Color = ({ id, title, color, rating, onRemove = f => f }) => {
return (
<section>
<h1>{title}</h1>
<button className="t-Button" onClick={() => onRemove(id)} >
<FaRemove />
</button>
<div style={{ height: 50, backgroundColor: color}} />
<StarRating selectedStars={rating} />
</section>
);
};

第 1 行使用 APEX icons fa-remove 新增一個 FaRemove 圖標組件。 接下來 7 ~ 9 行將 FaRemove 圖標包裝在一個按鈕中,並在此按鈕上添加 onClick 處理程序,這使我們可以調用 onRemove 函式。 當用戶單擊 “刪除” 按鈕時,通過第 3 行的 onRemove 函式屬性將事件往上傳送到根組件(CPS 延續傳遞風格),最終我們將在 App 根組件調用一個 onRemoveColor 函式將其想要刪除的顏色刪除。

這裡我們使 Color 組件保持純淨。它沒有狀態,可以很容易地在別的地方重複使用。Color 組件與用戶單擊 “刪除” 按鈕時發生甚麼事無關,它關心的只是通知父組件該事件已發生,並傳遞有關用戶希望刪除的顏色的資料,這裡傳遞的是顏色的 id。

現在,父組件有責任處理此事件:

ColorList
1
2
3
4
5
6
7
8
9
10
11
const ColorList = ({ colors = [], onRemoveColor = f => f }) => {
if(!colors.length) return <div>No Colors Listed.</div>;

return (
<div>
{
colors.map(color => <Color key={color.id} {...color} onRemove={onRemoveColor} />)
}
</div>
);
};

Color 組件的父級是 ColorList,該組件也無權訪問狀態。它只是將事件繼續傳遞給其父組件。它通過添加 onRemoveColor 函式屬性來實現此目的。如果 Color 組件調用 onRemove 屬性,則 ColorList 將延續調用其 onRemoveColor 屬性,並將刪除顏色的責任發送給其父組件 App。

ColorList 的父級是 App。 該組件是最終與狀態掛勾的組件,在這裡我們可以刪除狀態下的顏色。

App
1
2
3
4
5
6
7
8
9
10
11
12
13
const App = () => {
const [colors, setColors] = useState(colorData);

return (
<ColorList
colors={colors}
onRemoveColor={id => {
const newColors = colors.filter(color => color.id !== id);
setColors(newColors);
}}
/>
)
};

首先,我們在第 2 行添加一個 setColors 變數。請記住,useState 返回的陣列中的第二個參數是一個可以用來修改狀態的函式。當 ColorList 引發一個 onRemoveColor 事件時,我們使用參數中所要刪除的顏色的 id 來過濾,以排除使用者想要刪除的顏色。接下來,我們使用 setColors 函式更改狀態,將 colors 更改為新過濾的陣列。

更改 colors 陣列的狀態會使 App 組件重新呈現新的 colors。這些新的 colors 也將透過屬性傳遞到所有相關的子組件,進而引發整個樹狀結構的重新渲染。

Color Rating

如果要對存儲在 App 組件狀態中的顏色進行評分,則必須使用 onRate 事件重複上述刪除顏色的過程。

首先,我們將從被點擊的 FaStar 組件中觸發新的評分事件,並將事件往上傳送 Star 父組件,再經過 StarRating 組件,依序直到 App 根組件以修改顏色的評分 rating。

FaStar 與 Star 組件都是無狀態組件,可以被重複使用,無須變動。我們只需從 StarRating 組件開始新增 onRate 函式屬性。

onRate:StarRating
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const StarRating = ({ totalStars = 5, selectedStars = 0, onRate = f => f }) => {    
return (
<>
{createArray(totalStars).map((n, i) => (
<Star
key={i}
selected={selectedStars > i}
onSelect={() => onRate(i + 1)}
/>
))}
<p style={{fontSize: "1.5em", margin: "10px"}}>{selectedStars} of {totalStars} stars</p>
</>
);
};

第 8 行我們重新將 onSelect 屬性加進 Star 組件,並透過第 1 行的 onRate 函式屬性將事件與 rating 評分等級 (i + 1) 往上傳送給 StarRating 的父組件 Color。

onRate:Color
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Color = ({ id, title, color, rating, onRemove = f => f, onRate = f => f }) => {
return (
<section>
<h1>{title}</h1>
<button className="t-Button" onClick={() => onRemove(id)} >
<FaRemove />
</button>
<div style={{ height: 50, backgroundColor: color}} />
<StarRating
selectedStars={rating}
onRate={rating => onRate(id, rating)}
/>
</section>
);
};

在 Color 組件第 11 行將新的評級 rating 以及將要評級的顏色 id 通過第 1 行的 onRate 函式屬性傳遞給 Color 組件的父項 ColorList 組件。

在 ColorList 組件中,我們必須從各個顏色 Color 組件中捕獲 onRate 事件,然後通過第 1 行的 onRateColor 函式屬性將它們傳遞給其父級 App 組件:

onRate:ColorList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const ColorList = ({ colors = [], onRemoveColor = f => f, onRateColor = f => f }) => {
if(!colors.length) return <div>No Colors Listed.</div>;

return (
<div>
{
colors.map(color => (
<Color
key={color.id}
{...color}
onRemove={onRemoveColor}
onRate={onRateColor}
/>
))
}
</div>
);
};

最後,在將事件依序經過些組件往上傳遞之後,我們將到達應用程序存儲狀態的根組件 App,可以更改 colors 數據保存新的評分。

onRate:App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const App = () => {
const [colors, setColors] = useState(colorData);

return (
<ColorList
colors={colors}
onRateColor={(id, rating) => {
const newColors = colors.map(color => color.id === id ? {...color, rating} : color);
setColors(newColors);
}}
onRemoveColor={id => {
const newColors = colors.filter(color => color.id !== id);
setColors(newColors);
}}
/>
)
};

一旦我們的顏色陣列 colors 的狀態發生變化,相關的 UI 組件樹就會使用新數據進行渲染,新的評級將反映給用戶。

就像我們通過 props 屬性將數據往下傳遞到整個組件樹一樣,使用者的互動也可以通過函式屬性與數據一起往上傳遞回樹根。

使用 Forms 新增數據

成為一名 Web 開發人員總是必須使用表單從使用者那裡收集大量的信息,那麼將會使用 React 構建許多表單組件。DOM 所有可用的 HTML 表單元素也都可以作為 React 元素使用,因此可以用 JSX 呈現表單。

當需要在 React 中構建表單組件時,可以使用幾種模式。 模式之一使用稱為 refs 的 React 功能直接訪問 DOM 節點。在 React 中,ref 是一個物件,它存儲組件生命週期內的值。React 為我們提供了一個 useRef 掛鉤(hook),我們可以使用它來創建一個 ref 物件。

直接通過將 DOM 節點的 value 屬性設定值,稱為不受控制的組件(uncontrolled component),因為它使用 DOM 來保存表單值。有時使用不受控制的組件可以使您解決一些麻煩。 例如,您可能想與 React 之外的代碼共享一個表單及其值。 但是,在 React 中使用受控制組件(controlled component)是比較好的方法。

在受控制組件中,表單值由 React 而不是 DOM 管理。它們不需要我們使用 ref 物件,也不需要我們編寫命令性代碼。當使用受控組件時,添加表單驗證之類的功能要容易得多。

這裡,我們不展示不受控制組件,直接使用受控制組件。

受控制組件使用 React 狀態(state)保存表單的值。我們將用 useState 掛鉤建立兩個變數 title 與 color。此外,我們將也將定義可用於更改狀態的函式:setTitle 和 setColor。

AddColorForm
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
const AddColorForm = ({ onNewColor = f => f }) => {
const [title, setTitle] = useState("");
const [color, setColor] = useState("#000000");

const submit = e => {
e.preventDefault();
onNewColor(title, color);
setTitle("");
setColor("#000000");
};

return (
<form onSubmit={submit} >
<input
value={title}
onChange={ event => setTitle(event.target.value)}
type="text"
placeholder="color title..."
required
/>
<input
value={color}
onChange={ event => setColor(event.target.value)}
type="color"
required
/>
<button className="t-Button t-Button--hot margin-left-sm">Add</button>
</form>
);
};

每當這些 input 元素引發 onChange 事件時,我們將調用 setTitle 或 setColor 來更改狀態中的值。更改該值將導致該組件重新渲染,會立即在 input 元素中顯示新的值。

提交表單時,我們只需在第 7 行直接將 title 和 color 的狀態值作為參數傳遞給 onNewColor 函式屬性即可。

知道受控組件會經常重新渲染,你將知道避免向該組件添加一些漫長且昂貴的處理過程。當您嘗試優化 React 組件時,這些知識將會派上用場。

現在將 AddColorForm 加入我們的根組件 App,並定義 onNewColor 函式屬性。記得也將 Demo 模組的 uuid 函式加進來。

AddColorForm:App
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
const { uuid, colorData } = Demo;
...

const App = () => {
const [colors, setColors] = useState(colorData);

return (
<>
<AddColorForm
onNewColor={(title, color) => {
const newColors = [
...colors,
{
id: uuid(),
rating: 0,
title,
color
}
];
setColors(newColors);
}}
/>
<ColorList
colors={colors}
onRateColor={(id, rating) => {
const newColors = colors.map(color => color.id === id ? {...color, rating} : color);
setColors(newColors);
}}
onRemoveColor={id => {
const newColors = colors.filter(color => color.id !== id);
setColors(newColors);
}}
/>
</>
)
};

AddColorForm 組件通過 onNewColor 函式將 title 和 color 的值傳遞給父組件 App。調用 onNewColor 屬性時,我們會將新顏色保存在狀態中。

將狀態統一存儲在樹狀結構的根的位置是一種重要的模式,這是早期版本 React 的重要模式。通過屬性上下傳遞狀態是任何 React 開發人員必須知道的概念,是我們所有人都應該知道的方法。 但是,隨著 React 的發展以及我們的組件樹越來越大,遵循此原理逐漸變得不切實際。對於復雜的應用程序來說,許多開發人員很難在組件樹根的單個位置維護狀態。通過數十個組件將狀態上下傳遞是繁瑣且容易出錯的。

多數的 UI 元素都很複雜,樹的根通常離葉子很遠,將數據置於離使用數據組件許多層的位置,每個組件都必須收到只能傳遞給子組件的屬性,這將使我們的代碼變的臃腫,並使我們的 UI 難以擴展。

將狀態數據作為屬性傳遞通過每個組件,直到到達需要使用的組件,就像乘火車從台南到台北。在火車上,您會經過每個站,但直到到達目的地,您都不會下車。

搭飛機從台南飛往台北顯然效率較佳。這樣,您不必通過每個中間站,你直接飛越它們。

下次我們將用比較有效率的方式,直接飛越它們: React Context