0%

React State Management

數據活化我們的 React 組件。 沒有數據,我們構建的使用者介面就沒有用。我們的使用者介面是創建用來生成數據內容的工具。 為了為我們的數據內容創建者構建最好的工具,我們需要知道如何有效地操作和更改數據。

之前,我們建構了一個組件樹(component tree):數據能夠作為屬性流經整個組件層次結構。 屬性(properties)是整體構圖的一半。另一半則是狀態(state)。 React 應用程序的狀態由具有更改能力的數據驅動。 應用程序有了狀態,讓我們可以創建、修改、刪除現有的數據。

狀態和屬性相互之間具有聯繫性。當我們使用 React 應用程序時,我們會根據狀態和屬性這種關係,將所有的組件組合在一起。當組件樹的狀態更改時,屬性也會更改。 新數據流過組件樹,導致特定的終端組件和分支重新渲染以呈現反映新的內容。

我們將使用狀態來活化應用程序,創建有狀態組件(stateful components),將狀態由根組件往下發送到整個組件樹,及如何將使用者使用表單收集的互動數據由組件樹的末端往上回溯。

Star Rating Component

這裡要建立一個星級評分組件,五星級評分,可以讓我們評論東西的好壞。也能讓使用者驅動我們對內容的改進。

我們要建立一個 StarRating 組件,StarRating 組件將允許使用者根據特定的星星數對內容進行評分。不好的內容獲得一顆星。高度推薦的內容獲得五顆星。用戶可以透過單擊特定的星號來設置內容的等級。

首先我們需要一個星星圖標。Oracle APEX - Universal Theme 內建有許多 icons 可以直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="react-root"></div>

<script type="text/babel">
const { render } = ReactDOM;

const FaStar = ({ color }) => (
<span className="fa fa-star fa-2x" style={{color}}></span>
);

const StarRating = () => (
[
<FaStar color="gold" />,
<FaStar color="gold" />,
<FaStar color="gold" />,
<FaStar color="grey" />,
<FaStar color="grey" />
]
);

render(<StarRating />, document.getElementById("react-root"));
</script>

在這裡,我們創建了 FaStar 組件將 APEX Universal Theme 的圖標 fa-star 包裝起來。然後創建一個 StarRating 組件,該組件渲染了五個 FaStar 組件,產生五顆星星, 前三顆填滿了金色,後兩顆是灰色。

首先渲染了五顆星星,它們為我們提供了構建的基礎。 選定的星星填滿金色,未選擇的星星是灰色。

讓我們再創建一個 Star 組件,該組件會根據 selected 屬性自動填滿星星的顏色:

1
2
3
const Star = ({ selected = false }) => (
<FaStar color={selected ? "gold" : "grey"} />
);

Star 組件將渲染單個星星,並使用 selected 屬性為其填充適當的顏色。如果未將 selected 屬性傳遞給此組件,則假定該星星沒有被選,並且預設填充灰色。

5 星級評分很好,但 10 星級可以提供更詳細的評分。我們應該允許開發人員將組件添加到應用程序時,可以選擇希望使用的總星數。這可以透過在 StarRating 組件中添加 totalStars 屬性來實現。

1
2
3
4
5
const createArray = length => [...Array(length)];

const StarRating = ({ totalStars = 5 }) => (
createArray(totalStars).map((n, i) => <Star key={i} />)
);

我們添加了 createArray 函式。我們要做的就是提供要創建的陣列長度,然後用該長度取得一個新的陣列。 我們將此函式與 totalStars 屬性一起使用,就可以用陣列 map 渲染 Star 組件。 預設情況下,totalStars 等於 5,將渲染 5 個灰色星星。

useState Hook

現在要使星級評分組件可以單擊,這將允許使用者更改評分。

由於評分是一個會改變的值,因此我們將使用 React 狀態(state)來儲存和更改該值。我們要使用稱為 Hook 的 React 功能將狀態合併到組件中。 Hook 包含與組件樹分開的可重用代碼邏輯。它使我們能夠將所要處裡的功能連接到我們的組件。React 有幾個內建的 Hooks,我們可以直接使用它們。這裡,我們想向 React 組件添加狀態,因此我們要使用的第一個 Hook 是 React 的 useState。這個 Hook 已經包含在 react 程式包中,我們只需要將它加入我們的程式:

1
const { useState } = React;

使用者所選擇的星號代表等級。我們將創建一個名為 selectedStars 的狀態變數(state variable),它將保存用戶的評分。我們將透過將 useState Hook 直接添加到 StarRating 組件中來創建此狀態變數:

1
2
3
4
5
6
7
8
9
10
11
12
const StarRating = ({ totalStars = 5 }) => {
const [selectedStars] = useState(3);

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

我們只是將該組件與狀態掛鉤。useState 掛鉤是一個函式,調用它會返回一個陣列。返回的陣列的第一個值是我們要使用的狀態變數。在這裡,該變數為 selectedStars,也就是 StarRating 組件將顏色變為金色的星數。useState 返回一個陣列,我們可以利用陣列解構賦值,這使我們可以隨意命名狀態變數。我們調用 useState 函式時給的引數值是狀態變數的初始預設值。在這裡,selectedStars 初始預設值將設置為 3。

為了從使用者那裡獲得不同的評分,我們需要允許他們點擊我們的任何一顆星星。 所以,我們需要透過向 FaStar 組件中的星形圖標 fa-star 添加 onClick 處理程序來使星星可點擊:

1
2
3
const FaStar = ({ color, onSelect =  f => f }) => ( 
<span className="fa fa-star fa-2x" style={{color}} onClick={onSelect} ></span>
);

這裡,我們修改了星形圖標 fa-star 以包含 onClick 事件,這個事件的處理程序是由父組件傳入的 onSelect 屬性。 注意,此屬性是一個函式。 當用戶單擊星形圖標時,我們將調用此函式,該函式可以通知其父組件已單擊星形圖標。該函式的預設值為 f => f。 這只是一個偽造的函式,什麼也不做。它只是返回發送給它的任何參數。但是,如果我們未設置預設函式並且未定義 onSelect 屬性,則在我們單擊星形圖標時會發生錯誤,因為 onSelect 的值必須是一個函式。 即使 f => f 什麼也不做,它還是一個函式,可以調用它而不會引起錯誤。如果沒有定義 onSelect 屬性,沒有問題,React 將簡單地調用偽函式,而不會發生任何事情。

現在我們的星形圖標 fa-star 是可單擊的,我們將使用它來更改 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>
</>
);
};

為了更改 StarRating 組件的狀態,我們需要一個可以修改 selectedStars 值的函式。 useState 掛鉤返回的陣列中的第二項是可用於更改狀態值的函式。同樣,透過陣列解構賦值,我們可以隨心所欲地命名該函式。在這裡,我們稱其為 setSelectedStars,因為它就是用來設置 selectedStars 的值。

關於 Hooks,要記住的最重要的事情是,Hooks 可能導致掛鉤他們的組件重新渲染。每次我們調用 setSelectedStars 函式來更改 selectedStars 的值時,該掛鉤將重新調用 StarRating 函式組件,並且它將再次重新渲染。 這就是為什麼 Hooks 是殺手級功能的原因。當 Hook 中的數據發生更改時,他們會使用新數據重新渲染掛接它們的組件。

每次使用者單擊星形圖標 fa-star 時,將重新渲染 StarRating 組件。當使用者單擊星形圖標 fa-star 時,將調用該星星的 onSelect 屬性。 調用 onSelect 屬性時,我們將調用 setSelectedStars 函式並將剛選擇的星星編號當為函式的引數傳入。 我們使用 map 函數中的 i 變數來幫助我們計算該數字。當 map 函式渲染第一個 Star 時,i 的值為 0,我們需要在該值上加上 1 以獲取正確的 star 編號。 新的 selectedStars 的值發生變化會導致 StarRating 組件的重新渲染。

你如果現在單擊星形圖標,將不會發生任何作用,因為 FaStar 組件與 StarRating 組件並沒有直接關連,它是透過 Star 組件組成一個階層式組件樹,數據透過屬性流經整個組件層次結構。onSelect 屬性也必須透過 Star 組件流到 FaStar 組件,因為 FaStar 組件中的星形圖標 fa-star 才是使用者單擊的標的,onClick 事件是由此被觸發的。

1
2
3
const Star = ({ selected = false, onSelect = f => f }) => (
<FaStar color={selected ? "gold" : "grey"} onSelect={onSelect} />
);

整個程式碼現在如下:

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
<div id="react-root"></div>

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

const FaStar = ({ color, onSelect = f => f }) => (
<span className="fa fa-star fa-2x" style={{color}} onClick={onSelect} ></span>
);

const Star = ({ selected = false, onSelect = f => f }) => (
<FaStar color={selected ? "gold" : "grey"} onSelect={onSelect} />
);

const createArray = length => [...Array(length)];

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>
</>
);
};

render(<StarRating totalStars={5} />, document.getElementById("react-root"));
</script>

你可以將第 34 行的 totalStars 改為 10 就變成 10 星級評分了。

在 v16.8.0 之前的 React 早期版本中,向組件添加狀態的唯一方法是使用類組件(class component)。這不僅需要大量的代碼,而且還使得跨組件重用功能變得更加困難。Hooks 旨在透過將功能掛鉤到函式組件來解決類組件所帶來的問題。目前,類組件代碼仍然有效,但我們已不再需要它們,可能會有一天類組件會正式被棄用。

函式組件和 Hooks 是 React 的未來。

延續傳遞風格(CPS)

在這個星級評分組件中我們用到了 JavaScript 的一種很重要的概念,延續傳遞風格(Continuation-passing style),簡稱 CPS。

JavaScript 的回呼 (callback) 是作為參數傳遞給其他函式,並在操作完成時呼叫以傳遞結果。在函式化的程式設計風格,這種傳遞結果的方式稱為延續傳遞風格(Continuation-passing style),簡稱 CPS 。這是ㄧ個通用的概念,而不一定與非同步操作相關。事實上,它只是指出操作結果是以傳給另一個函式(回呼)的方式來傳遞,而不是直接傳給呼叫者。

程式碼的第 26 行的 onSelect 屬性是一個函式,這個函式我們是定義在 StarRating 組件上,以組件屬性的方式傳遞到 FaStar 組件中的星形圖標 fa-star,當使用者單擊星形圖標 fa-star 時將結果透過函式回傳 StarRating 根組件。每一個星形圖標 fa-star 的函式看起來都一樣,但它其實是一個閉包 (closure),函式 ( ) => setSelectedStars( i + 1 ) 中 i 變數的值都不同。

閉包與延續傳遞風格在 JavaScript 中是很重要的概念,需要花點時間熟悉它。

思考一下這一段 JavaScript:

1
2
3
const stars = [...Array(5)].map((n, i) => () => console.log(`Star => ${i}`));

stars[2]();