此章先以一個完整的例子來全面了解下React組件開發的流程,主要是以代碼為主,在不同的章節中會把重點標出來,要完成的例子如下,也可從官網中找到。
React組件開發流程
這只是一個通用流程,在熟悉后不需要完全遵從。
- 將 UI 拆解為組件層級結構
- 使用 React 構建一個靜態版本
- 找出 UI 精簡且完整的 state 表示
- 驗證 state 應該被放置在哪里
- 添加反向數據流
組件開發流程示例
將 UI 拆解為組件層級結構
- FilterableProductTable(灰色)包含完整的應用,最外層元素
- SearchBar(藍色)搜索輸入框和復選按鈕,獲取用戶輸入。
- ProductTable(淡紫色)產品列表,根據用戶輸入,展示和過濾清單。
- ProductCategoryRow(綠色)展示每個類別的表頭。
- ProductRow(黃色)展示每個產品的行。
拆分后構建一種上述的樹狀結構。
使用 React 構建一個靜態版本
在簡單的例子中,自上而下構建通常更簡單;而在大型項目中,自下而上構建更簡單。這里使用自下向上方式構建
ProductCategoryRow
category
參數為字符串,如Fruits或Vegetables,從下面的數據中取
{category: “Vegetables”, price: “$1”, stocked: true, name: “Peas”}
function ProductCategoryRow({ category }) {return (<tr><th colSpan="2">{category}</th></tr>);
}
ProductRow
product
參數為是一完整數據,如下面數據
{category: “Vegetables”, price: “$1”, stocked: true, name: “Peas”}
function ProductRow({ product }) {const name = product.stocked ? product.name :<span style={{ color: 'red' }}>{product.name}</span>;return (<tr><td>{name}</td><td>{product.price}</td></tr>);
}
ProductTable
products
參數為產品數據組,最后返回一個完整的table表格
function ProductTable({ products }) {const rows = []; //包含多個ProductCategoryRow組件和ProductRow組件,共同組成一個單獨的表格let lastCategory = null;products.forEach((product) => {if (product.category !== lastCategory) {rows.push(<ProductCategoryRowcategory={product.category}key={product.category} />);}rows.push(<ProductRowproduct={product}key={product.name} />);lastCategory = product.category;});return (<table><thead><tr><th>Name</th><th>Price</th></tr></thead><tbody>{rows}</tbody></table>);
}
SearchBar
無參數,包含一個輸入框和一個復選框
function SearchBar() {return (<form><input type="text" placeholder="Search..." /><label><input type="checkbox" />{' '}Only show products in stock</label></form>);
}
FilterableProductTable
products
參數為產品數據組
function FilterableProductTable({ products }) {return (<div><SearchBar /><ProductTable products={products} /></div>);
}
發布組件
最頂層組件(FilterableProductTable)將接收數據模型作為其 prop。這被稱之為 單向數據流。
const PRODUCTS = [{category: "Fruits", price: "$1", stocked: true, name: "Apple"},{category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},{category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},{category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},{category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},{category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];export default function App() {return <FilterableProductTable products={PRODUCTS} />;
}
找出 UI 精簡且完整的 state 表示
有一個規律可以了解下:
- 隨著時間推移 保持不變?如此,便不是 state。
- 通過 props 從父組件傳遞?如此,便不是 state。
- 是否可以基于已存在于組件中的 state 或者 props 進行計算?如此,它肯定不是state!
剩下的可能是 state。所以
讓我們再次一條條驗證它們:
- 原始列表中的產品 被作為 props 傳遞,所以不是 state。
- 搜索文本似乎應該是 state,因為它會隨著時間的推移而變化,并且無法從任何東西中計算出來。
- 復選框的值似乎是 state,因為它會隨著時間的推移而變化,并且無法從任何東西中計算出來。
- 過濾后列表中的產品 不是 state,因為可以通過被原始列表中的產品,根據搜索框文本和復選框的值進行計算。
- props 像是你傳遞的參數 至函數。它們使父組件可以傳遞數據給子組件,定制它們的展示。舉個例子,Form 可以傳遞 color prop 至 Button。
- state 像是組件的內存。它使組件可以對一些信息保持追蹤,并根據交互來改變。舉個例子,Button 可以保持對 isHovered state 的追蹤。
驗證 state 應該被放置在哪里
驗證哪個組件是通過改變 state 實現可響應的,或者 擁有 這個 state。
記住:React 使用單向數據流,通過組件層級結構從父組件傳遞數據至子組件。要搞清楚哪個組件擁有哪個 state。
驗證使用 state 的組件:
- ProductTable 需要基于 state (搜索文本和復選框值) 過濾產品列表。
- SearchBar 需要展示 state (搜索文本和復選框值)。
- 尋找它們的父組件:它們的第一個共同父組件為 FilterableProductTable。
決定 state 放置的地方:我們將過濾文本和勾選 state 的值放置于 FilterableProductTable 中。所以 state 將被放置在 FilterableProductTable。
import { useState } from 'react';function FilterableProductTable({ products }) {const [filterText, setFilterText] = useState('');const [inStockOnly, setInStockOnly] = useState(false);return (<div><SearchBarfilterText={filterText}inStockOnly={inStockOnly} /><ProductTableproducts={products}filterText={filterText}inStockOnly={inStockOnly} /></div>);
}
然后,filterText 和 inStockOnly 作為 props 傳遞至 ProductTable 和 SearchBar。
添加反向數據流
簡單來講就是添加事件,當用戶更改表單輸入時,state 將更新以反映這些更改。state 由 FilterableProductTable 所擁有,所以只有它可以調用 setFilterText 和 setInStockOnly。
function SearchBar({filterText,inStockOnly,onFilterTextChange,onInStockOnlyChange
}) {return (<form><inputtype="text"value={filterText}placeholder="搜索"onChange={(e) => onFilterTextChange(e.target.value)}/><label><inputtype="checkbox"checked={inStockOnly}onChange={(e) => onInStockOnlyChange(e.target.checked)}
完整代碼如下
這里的事件反向流是用于操作state的,這東西又是私有的,所以需要一個事件函數傳遞的過程,即
- 自定義組件FilterableProductTable定義了一個filterText變量;
- 在FilterableProductTable中包含子組件SearchBar,子組件中有這個輸入框,所以需要把filterText的設置函數傳遞進去;
- SearchBar 組件需要定義一個參數接收state方法,這個方法供原生的onChange觸發
import { useState } from 'react';function FilterableProductTable({ products }) {const [filterText, setFilterText] = useState('');const [inStockOnly, setInStockOnly] = useState(false);return (<div><SearchBarfilterText={filterText}inStockOnly={inStockOnly}onFilterTextChange={setFilterText}onInStockOnlyChange={setInStockOnly} /><ProductTableproducts={products}filterText={filterText}inStockOnly={inStockOnly} /></div>);
}function ProductCategoryRow({ category }) {return (<tr><th colSpan="2">{category}</th></tr>);
}function ProductRow({ product }) {const name = product.stocked ? product.name :<span style={{ color: 'red' }}>{product.name}</span>;return (<tr><td>{name}</td><td>{product.price}</td></tr>);
}function ProductTable({ products, filterText, inStockOnly }) {const rows = [];let lastCategory = null;products.forEach((product) => {if (product.name.toLowerCase().indexOf(filterText.toLowerCase()) === -1) {return;}if (inStockOnly && !product.stocked) {return;}if (product.category !== lastCategory) {rows.push(<ProductCategoryRowcategory={product.category}key={product.category} />);}rows.push(<ProductRowproduct={product}key={product.name} />);lastCategory = product.category;});return (<table><thead><tr><th>Name</th><th>Price</th></tr></thead><tbody>{rows}</tbody></table>);
}function SearchBar({filterText,inStockOnly,onFilterTextChange,onInStockOnlyChange}) {return (<form><inputtype="text"value={filterText} placeholder="Search..."onChange={(e) => onFilterTextChange(e.target.value)} /><label><inputtype="checkbox"checked={inStockOnly}onChange={(e) => onInStockOnlyChange(e.target.checked)} />{' '}Only show products in stock</label></form>);
}const PRODUCTS = [{category: "Fruits", price: "$1", stocked: true, name: "Apple"},{category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},{category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},{category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},{category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},{category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];export default function App() {return <FilterableProductTable products={PRODUCTS} />;
}