React 可以改变你对所看设计和所构建应用程序的思考方式。当你使用 React 构建用户界面时,首先会将其分解成称为组件的片段。然后,你会描述每个组件的不同视觉状态。最后,你将组件连接在一起,以便数据流经它们。在本教程中,我们将指导你完成使用 React 构建可搜索的产品数据表的思维过程。
从模型开始
假设你已经拥有了一个 JSON API 和一个来自设计师的模型。
JSON API 返回了一些看起来像这样的数据
[
{ 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" }
]
模型看起来像这样
要在 React 中实现 UI,你通常会遵循相同的五个步骤。
步骤 1:将 UI 分解为组件层次结构
首先,在模型的每个组件和子组件周围绘制方框并为其命名。如果你与设计师合作,他们可能已经在他们的设计工具中命名了这些组件。向他们询问!
根据你的背景,你可以从不同的角度思考将设计拆分为组件。
- 编程—使用相同的技术来决定是否应该创建一个新的函数或对象。其中一项技术是单一职责原则,即一个组件应该理想情况下只做一件事。如果它最终变得越来越庞大,它应该分解成更小的子组件。
- CSS—考虑你将为其创建哪些类选择器。(不过,组件的粒度要细一些。)
- 设计—考虑如何组织设计的图层。
如果你的 JSON 结构良好,你会经常发现它会自然映射到你的 UI 的组件结构。这是因为 UI 和数据模型通常具有相同的架构,即相同的形状。将你的 UI 分解成组件,其中每个组件对应你的数据模型的一部分。
此屏幕上有五个组件
FilterableProductTable
(灰色)包含整个应用程序。SearchBar
(蓝色)接收用户输入。ProductTable
(淡紫色)根据用户输入显示和过滤列表。ProductCategoryRow
(绿色)为每个类别显示一个标题。ProductRow
(黄色)为每个产品显示一行。
如果你查看 ProductTable
(淡紫色),你会发现表头(包含“名称”和“价格”标签)不是它自己的组件。这是个人喜好问题,你可以选择任一种方式。在本例中,它是 ProductTable
的一部分,因为它出现在 ProductTable
的列表内。但是,如果该表头变得复杂(例如,如果你添加排序功能),你可以将其移到它自己的 ProductTableHeader
组件中。
现在你已经确定了模型中的组件,将它们排列成层次结构。在模型中出现在另一个组件内的组件应该作为子组件出现在层次结构中。
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
步骤 2:在 React 中构建静态版本
现在你有了组件层次结构,是时候实现你的应用程序了。最直接的方法是构建一个版本,它从你的数据模型中渲染 UI,而无需添加任何交互性……至少现在还没有!通常先构建静态版本,然后再添加交互性更容易。构建静态版本需要很多输入,不需要思考,而添加交互性则需要很多思考,不需要太多输入。
为了构建一个静态版本的应用程序,它从你的数据模型中渲染 UI,你需要构建 组件,这些组件会重用其他组件并使用 props 传递数据。Props 是从父组件传递数据到子组件的一种方式。(如果你熟悉 state 的概念,不要使用 state 来构建这个静态版本。State 专门用于交互性,即随着时间的推移而改变的数据。由于这是应用程序的静态版本,所以你不需要它。)
你可以“自顶向下”构建,从构建层次结构中更高级别的组件开始(例如 FilterableProductTable
),或者“自底向上”构建,从层次结构中更低级别的组件开始(例如 ProductRow
)。在更简单的示例中,通常更容易自顶向下构建,而在更大的项目中,更容易自底向上构建。
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 }) { const rows = []; let lastCategory = null; products.forEach((product) => { if (product.category !== lastCategory) { rows.push( <ProductCategoryRow category={product.category} key={product.category} /> ); } rows.push( <ProductRow product={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() { return ( <form> <input type="text" placeholder="Search..." /> <label> <input type="checkbox" /> {' '} Only show products in stock </label> </form> ); } function FilterableProductTable({ products }) { return ( <div> <SearchBar /> <ProductTable products={products} /> </div> ); } 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} />; }
(如果这段代码看起来很吓人,请先阅读 快速入门!)
构建完组件后,你将拥有一个可重用的组件库,用于呈现你的数据模型。由于这是一个静态应用程序,组件只返回 JSX。层次结构顶部的组件(FilterableProductTable
)将接收你的数据模型作为 prop。这被称为 *单向数据流*,因为数据从顶层组件向下流向树底部的组件。
步骤 3:找到 UI 状态的最小但完整的表示
为了使 UI 具有交互性,你需要允许用户更改底层数据模型。为此,你将使用 *状态*。
将状态视为应用程序需要记住的最小一组变化数据。构建状态的最重要原则是保持其 DRY(不要重复自己)。找出应用程序需要的状态的绝对最小表示,并按需计算所有其他内容。例如,如果你正在构建一个购物清单,可以将商品存储为状态中的数组。如果你还想显示清单中的商品数量,不要将商品数量存储为另一个状态值,而是读取数组的长度。
现在,思考一下此示例应用程序中的所有数据片段
- 原始产品列表
- 用户输入的搜索文本
- 复选框的值
- 过滤后的产品列表
哪些是状态?识别出哪些不是状态
- 它是否 随时间保持不变? 如果是,则它不是状态。
- 它是否 通过 prop 从父级传递? 如果是,则它不是状态。
- 你能根据组件中现有的状态或 prop 计算它吗?如果是,则它 *绝对* 不是状态!
剩下的可能是状态。
让我们再次逐个分析
- 原始产品列表 作为 prop 传入,因此它不是状态。
- 搜索文本似乎是状态,因为它会随时间变化,并且无法从任何东西中计算出来。
- 复选框的值似乎是状态,因为它会随时间变化,并且无法从任何东西中计算出来。
- 过滤后的产品列表 不是状态,因为它可以通过获取原始产品列表并根据搜索文本和复选框的值对其进行过滤来计算。
这意味着只有搜索文本和复选框的值才是状态!做得好!
深入挖掘
React 中有两种类型的“模型”数据:prop 和状态。两者截然不同
- prop 就像传递给函数的参数一样。 它们允许父组件将数据传递给子组件并自定义其外观。例如,一个
Form
可以传递一个color
prop 给一个Button
。 - 状态 就像组件的记忆一样。 它允许组件跟踪一些信息并在响应交互时更改它。例如,一个
Button
可能会跟踪isHovered
状态。
prop 和状态是不同的,但它们协同工作。父组件通常会将一些信息保存在状态中(以便可以更改它),并将 *它向下* 传递给子组件作为它们的 prop。如果在第一次阅读时,两者之间的区别仍然感觉模糊,这是正常的。这需要一些练习才能真正理解!
步骤 4:确定状态应该存在的位置
确定应用程序的最小状态数据后,你需要确定哪个组件负责更改此状态,或者 *拥有* 状态。请记住:React 使用单向数据流,将数据从父组件向下传递到组件层次结构中的子组件。可能无法立即清楚哪个组件应该拥有哪个状态。如果你不熟悉此概念,这可能具有挑战性,但你可以通过以下步骤找出答案!
对于应用程序中的每个状态片段
- 确定 *每个* 基于该状态呈现内容的组件。
- 找到它们最近的共同父组件,即层次结构中位于它们上面的组件。
- 确定状态应该存在的位置
- 通常,你可以将状态直接放入它们的共同父组件中。
- 你也可以将状态放入位于其共同父组件之上的某个组件中。
- 如果你找不到适合拥有状态的组件,请创建一个专门用于保存状态的全新组件,并将其添加到层次结构中位于共同父组件之上的某个位置。
在上一步骤中,你在此应用程序中找到了两个状态片段:搜索输入文本和复选框的值。在此示例中,它们始终一起出现,因此将它们放入同一个位置是有意义的。
现在,让我们按照我们的策略来执行它们
- 识别使用状态的组件
ProductTable
需要根据该状态(搜索文本和复选框值)过滤产品列表。SearchBar
需要显示该状态(搜索文本和复选框值)。
- 找到它们的共同父组件: 两个组件共享的第一个父组件是
FilterableProductTable
。 - 确定状态存在的位置: 我们将在
FilterableProductTable
中保留过滤器文本和选中状态值。
因此,状态值将存在于 FilterableProductTable
中。
使用 useState() Hook
将状态添加到组件中。 Hook 是允许你“挂钩”React 的特殊函数。在 FilterableProductTable
顶部添加两个状态变量,并指定它们的初始状态
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
然后,将 filterText
和 inStockOnly
作为 prop 传递给 ProductTable
和 SearchBar
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>
你可以开始看到应用程序的行为方式。在下面的沙盒代码中,将 filterText
的初始值从 useState('')
修改为 useState('fruit')
。你将看到搜索输入文本和表格都已更新
import { useState } from 'react'; function FilterableProductTable({ products }) { const [filterText, setFilterText] = useState(''); const [inStockOnly, setInStockOnly] = useState(false); return ( <div> <SearchBar filterText={filterText} inStockOnly={inStockOnly} /> <ProductTable products={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( <ProductCategoryRow category={product.category} key={product.category} /> ); } rows.push( <ProductRow product={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 }) { return ( <form> <input type="text" value={filterText} placeholder="Search..."/> <label> <input type="checkbox" checked={inStockOnly} /> {' '} 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} />; }
请注意,编辑表单目前尚不可用。上面的沙盒中有一个控制台错误,解释了原因
在上面的沙箱中,ProductTable
和 SearchBar
读取 filterText
和 inStockOnly
属性来渲染表格、输入框和复选框。例如,以下是如何 SearchBar
填充输入框的值
function SearchBar({ filterText, inStockOnly }) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."/>
但是,您还没有添加任何代码来响应用户的操作,例如键入。这将是您的最后一步。
步骤 5:添加反向数据流
目前,您的应用程序使用道具和状态向下流动层次结构,可以正确渲染。但是,要根据用户输入更改状态,您需要支持数据以另一种方式流动:层次结构中深层的表单组件需要更新 FilterableProductTable
中的状态。
React 使这种数据流显式,但它需要比双向数据绑定多打一点字。如果您尝试在上面的示例中键入或选中复选框,您会发现 React 忽略了您的输入。这是故意的。通过编写 <input value={filterText} />
,您已将 input
的 value
属性始终设置为等于 filterText
状态(从 FilterableProductTable
传入)。由于 filterText
状态从未设置,因此输入从未改变。
您希望在用户更改表单输入时,状态更新以反映这些更改。该状态由 FilterableProductTable
拥有,因此只有它可以调用 setFilterText
和 setInStockOnly
。要让 SearchBar
更新 FilterableProductTable
的状态,您需要将这些函数传递给 SearchBar
。
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />
在 SearchBar
中,您将添加 onChange
事件处理程序,并从它们设置父状态。
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange
}) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={(e) => onInStockOnlyChange(e.target.checked)}
现在应用程序完全可以正常工作!
import { useState } from 'react'; function FilterableProductTable({ products }) { const [filterText, setFilterText] = useState(''); const [inStockOnly, setInStockOnly] = useState(false); return ( <div> <SearchBar filterText={filterText} inStockOnly={inStockOnly} onFilterTextChange={setFilterText} onInStockOnlyChange={setInStockOnly} /> <ProductTable products={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( <ProductCategoryRow category={product.category} key={product.category} /> ); } rows.push( <ProductRow product={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> <input type="text" value={filterText} placeholder="Search..." onChange={(e) => onFilterTextChange(e.target.value)} /> <label> <input type="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} />; }
您可以在 添加交互性 部分了解有关处理事件和更新状态的所有内容。
从这里去哪里
这只是对如何使用 React 构建组件和应用程序的思维方式的简要介绍。您可以 立即开始一个 React 项目 或者 深入了解在本教程中使用的所有语法。