第九篇 快速开始-用React的方式思考
用React的方式思考
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 分成多个组件,其中每个组件都与您的数据模型的一部分相匹配。
此屏幕上有五个组件:
- 1.FilterableProductTable(灰色)包含整个应用程序。
- 2.SearchBar(蓝色)接收用户输入。
- 3.ProductTable (淡紫色) 根据用户输入显示和过滤列表。
- 4.ProductCategoryRow(绿色)显示每个类别的标题。
- 5.ProductRow(黄色)为每个产品显示一行。
如果查看 ProductTable (淡紫色),您会发现表头(包含“Name”和“Price”标签)不是它自己的组件。 这是一个偏好问题,你可以选择任何一种方式。 对于此示例,它是 ProductTable 的一部分,因为它出现在 ProductTable 的列表中。 但是,如果此标头变得复杂(例如,如果您添加排序),则将其作为自己的 ProductTableHeader 组件是有意义的。
现在您已经确定了模型中的组件,将它们排列成层次结构。 出现在模型中另一个组件中的组件应该在层次结构中显示为子组件:
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
第2步:用React构建静态版本
现在您有了组件层次结构,是时候实施您的应用程序了。 最直接的方法是构建一个从您的数据模型呈现 UI 的版本,而不添加任何交互性……但是! 首先构建静态版本然后单独添加交互性通常更容易。 构建静态版本需要大量输入而不是思考,但添加交互性需要大量思考而不是大量输入。
要构建呈现数据模型的应用程序的静态版本,您需要构建可重用其他组件并使用 props 传递数据的组件。props是一种将数据从父母传递给孩子的方式。 (如果您熟悉状态的概念,请完全不要使用状态来构建此静态版本。状态仅保留用于交互性,即随时间变化的数据。由于这是应用程序的静态版本 ,你不需要它。)
您可以通过从构建层次结构中较高的组件(如 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) 会将您的数据模型作为props。 这称为单向数据流,因为数据从顶层组件向下流到树底部的组件。
"陷阱
此时,您不应使用任何状态值。 这是下一步的任务!
第3步:找到最小但完整的 UI 状态表示
要使 UI 具有交互性,您需要让用户更改您的基础数据模型。 您将为此使用状态。
将状态视为您的应用需要记住的最小变化数据集。 构建状态最重要的原则是保持DRY(不要重复自己)。 找出应用程序所需状态的绝对最小表示,并按需计算其他所有内容。 例如,如果你正在构建一个购物清单,你可以将项目存储为状态数组。 如果您还想显示列表中的项目数,请不要将项目数存储为另一个状态值——而是读取数组的长度。
现在想想这个示例应用程序中的所有数据片段:
- 原始产品清单
- 用户输入的搜索文本
- 复选框的值
- 过滤后的产品列表
其中哪些是状态? 识别那些不是的:
- 它会随着时间的推移保持
不变
吗? 如果是这样,它就不是状态。 - 它是通过
props
从父母那里传来的吗? 如果是这样,它就不是状态。 - 您可以根据组件中的现有状态或道具来
计算
它吗? 如果是这样,那肯定不是状态!
剩下的可能是状态。
让我们再一一回顾:
- 原始的产品列表作为 props 传入,所以它不是状态。
- 搜索文本似乎是有状态的,因为它会随着时间的推移而变化,并且无法从任何东西中计算出来。
- 复选框的值似乎是状态,因为它会随着时间而变化,并且无法从任何东西中计算出来。
- 过滤后的产品列表不是状态,因为它可以通过获取原始产品列表并根据搜索文本和复选框的值对其进行过滤来计算。
这意味着只有搜索文本和复选框的值是状态! 做得很好!
!深度探索:Props与state
React 中有两种类型的“模型”数据:props 和 state。 两者非常不同:
- props就像你传递给函数的参数。 它们让父组件将数据传递给子组件并自定义其外观。 例如,Form 可以将颜色属性传递给 Button。
- 状态就像一个组件的内存。 它允许组件跟踪某些信息并根据交互更改它。 例如,Button 可能会跟踪 isHovered 状态。
Props 和 state 是不同的,但它们一起工作。 父组件通常会在状态中保留一些信息(以便它可以更改它),并将其作为道具传递给子组件。 如果第一次阅读时仍然感觉差异模糊,那也没关系。 它需要一些练习才能真正深入理解!
第4步:确定state应该放在哪里
确定应用程序的最小状态数据后,您需要确定哪个组件负责更改此状态或拥有该状态。 记住:React 使用单向数据流,将数据从父组件向下传递到子组件。 可能不会立即清楚哪个组件应该拥有什么状态。 如果您是这个概念的新手,这可能具有挑战性,但您可以按照以下步骤弄明白!
对于应用程序中的每个状态:
- 识别基于该状态呈现某些内容的每个组件。
- 找到它们最接近的公共父组件——在层次结构中位于它们之上的组件。
-
- 决定状态应该在哪里:
- 通常,您可以将状态直接放入它们的共同父级中。
- 您还可以将状态放入其共同父级之上的某个组件中。
- 如果你找不到一个组件让拥有状态变得有意义,那么创建一个新的组件来保存状态,并将它添加到公共父组件之上的层次结构中的某个地方。
在上一步中,您在该应用程序中发现了两个状态:搜索输入文本和复选框的值。 在此示例中,它们总是一起出现,因此更容易将它们视为一个单独的状态。
现在让我们来看看我们针对这个状态的策略:
-
- 识别使用状态的组件:
- ProductTable 需要根据该状态(搜索文本和复选框值)过滤产品列表。
- SearchBar 需要显示该状态(搜索文本和复选框值)。
- 找到它们共同的父组件:两个组件共享的第一个父组件是 FilterableProductTable。
- 决定状态所在的位置:我们将在 FilterableProductTable 中保留过滤器文本和选中的状态值。
因此状态值将存在于 FilterableProductTable 中。
使用 useState() Hook 向组件添加状态。 Hooks让你“挂钩”到组件的渲染周期。 在 FilterableProductTable 的顶部添加两个状态变量并指定应用程序的初始状态:
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
然后,将 filterText 和 inStockOnly 作为 props 传递给 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} />;
}
请注意,编辑表单还不起作用。 上面的沙箱中有一个控制台错误,解释了原因:
Console
X You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field.
在上面的沙盒中,ProductTable 和 SearchBar 读取 filterText 和 inStockOnly 属性来呈现表格、输入和复选框。 例如,以下是 SearchBar 填充输入值的方式:
function SearchBar({ filterText, inStockOnly }) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="Search..."/>
但是,您还没有添加任何代码来响应用户的输入操作。 这将是您的最后一步。
第5步:添加反向数据流
当前,您的应用程序可以正确呈现 props 和 state 在层次结构中向下流动。 但是要根据用户输入更改状态,您将需要支持以其他方式流动的数据:层次结构深处的表单组件需要更新 FilterableProductTable 中的状态。
React 使此数据流显式,但它需要比双向数据绑定多一点的输入。 如果您尝试键入或选中上面示例中的框,您会看到 React 会忽略您的输入。 这是故意的。 通过编写 <input value={filterText} />,您已将输入的值属性设置为始终等于从 FilterableProductTable 传入的 filterText 状态。 由于从未设置 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 事件处理程序并从中设置父状态:
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)} />
现在应用程序完全可以工作了!
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 项目,或者更深入地研究本教程中使用的所有语法。