數據活化我們的 React 組件。 沒有數據,我們構建的使用者介面就沒有用。我們的使用者介面是創建用來生成數據內容的工具。 為了為我們的數據內容創建者構建最好的工具,我們需要知道如何有效地操作和更改數據。
之前,我們建構了一個組件樹(component tree):數據能夠作為屬性流經整個組件層次結構。 屬性(properties)是整體構圖的一半。另一半則是狀態(state)。 React 應用程序的狀態由具有更改能力的數據驅動。 應用程序有了狀態,讓我們可以創建、修改、刪除現有的數據。
狀態和屬性相互之間具有聯繫性。當我們使用 React 應用程序時,我們會根據狀態和屬性這種關係,將所有的組件組合在一起。當組件樹的狀態更改時,屬性也會更改。 新數據流過組件樹,導致特定的終端組件和分支重新渲染以呈現反映新的內容。
我們將使用狀態來活化應用程序,創建有狀態組件(stateful components),將狀態由根組件往下發送到整個組件樹,及如何將使用者使用表單收集的互動數據由組件樹的末端往上回溯。
Star Rating Component
這裡要建立一個星級評分組件,五星級評分,可以讓我們評論東西的好壞。也能讓使用者驅動我們對內容的改進。
我們要建立一個 StarRating 組件,StarRating 組件將允許使用者根據特定的星星數對內容進行評分。不好的內容獲得一顆星。高度推薦的內容獲得五顆星。用戶可以透過單擊特定的星號來設置內容的等級。
首先我們需要一個星星圖標。Oracle APEX - Universal Theme 內建有許多 icons 可以直接使用。
1 | <div id="react-root"></div> |
在這裡,我們創建了 FaStar 組件將 APEX Universal Theme 的圖標 fa-star 包裝起來。然後創建一個 StarRating 組件,該組件渲染了五個 FaStar 組件,產生五顆星星, 前三顆填滿了金色,後兩顆是灰色。
首先渲染了五顆星星,它們為我們提供了構建的基礎。 選定的星星填滿金色,未選擇的星星是灰色。
讓我們再創建一個 Star 組件,該組件會根據 selected 屬性自動填滿星星的顏色:
1 | const Star = ({ selected = false }) => ( |
Star 組件將渲染單個星星,並使用 selected 屬性為其填充適當的顏色。如果未將 selected 屬性傳遞給此組件,則假定該星星沒有被選,並且預設填充灰色。
5 星級評分很好,但 10 星級可以提供更詳細的評分。我們應該允許開發人員將組件添加到應用程序時,可以選擇希望使用的總星數。這可以透過在 StarRating 組件中添加 totalStars 屬性來實現。
1 | const createArray = length => [...Array(length)]; |
我們添加了 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 | const StarRating = ({ totalStars = 5 }) => { |
我們只是將該組件與狀態掛鉤。useState 掛鉤是一個函式,調用它會返回一個陣列。返回的陣列的第一個值是我們要使用的狀態變數。在這裡,該變數為 selectedStars,也就是 StarRating 組件將顏色變為金色的星數。useState 返回一個陣列,我們可以利用陣列解構賦值,這使我們可以隨意命名狀態變數。我們調用 useState 函式時給的引數值是狀態變數的初始預設值。在這裡,selectedStars 初始預設值將設置為 3。
為了從使用者那裡獲得不同的評分,我們需要允許他們點擊我們的任何一顆星星。 所以,我們需要透過向 FaStar 組件中的星形圖標 fa-star 添加 onClick 處理程序來使星星可點擊:
1 | const FaStar = ({ color, onSelect = f => f }) => ( |
這裡,我們修改了星形圖標 fa-star 以包含 onClick 事件,這個事件的處理程序是由父組件傳入的 onSelect 屬性。 注意,此屬性是一個函式。 當用戶單擊星形圖標時,我們將調用此函式,該函式可以通知其父組件已單擊星形圖標。該函式的預設值為 f => f。 這只是一個偽造的函式,什麼也不做。它只是返回發送給它的任何參數。但是,如果我們未設置預設函式並且未定義 onSelect 屬性,則在我們單擊星形圖標時會發生錯誤,因為 onSelect 的值必須是一個函式。 即使 f => f 什麼也不做,它還是一個函式,可以調用它而不會引起錯誤。如果沒有定義 onSelect 屬性,沒有問題,React 將簡單地調用偽函式,而不會發生任何事情。
現在我們的星形圖標 fa-star 是可單擊的,我們將使用它來更改 StarRating 的狀態。
1 | const StarRating = ({ totalStars = 5 }) => { |
為了更改 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 | const Star = ({ selected = false, onSelect = f => f }) => ( |
整個程式碼現在如下:
1 | <div id="react-root"></div> |
你可以將第 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 | const stars = [...Array(5)].map((n, i) => () => console.log(`Star => ${i}`)); |