React 编程思想 #2
React 编程思想 #2
接上文,已经实现了一个静态的页面,现在就要给页面加上交互了。
寻找 State
状态是应用需要记录的最小变化,构建状态的最重要的原则是 DRY(Don’t Repeat Yourself,不要重复自己)。对于一个应用,构建出它的状态的绝对最小表示,并通过这些状态计算其他需要的内容。例如,如果您正在构建一个购物列表,则可以将项目存储为状态中的数组。如果您还想显示列表中的项数,请不要将项数存储为另一个状态值,而是读取数组的长度。
以上来自 React 的官方文档对状态的定义,简单来说,状态是组件中不可计算的变量,当这些变量改变时,需要动态改变页面的内容,即重新渲染页面。
现在回想示例程序中的所有数据:
- 原始的产品列表;
- 用户输入的搜索文本;
- 单选框的值;
- 经过筛选的产品列表;
这里面哪些属于状态?状态需要满足以下要求:
- 随着时间推移,它会发生改变;
- 它不能是由父组件通过 props 传递来的;
- Don’t Repeat Yourself,它不应该能从任何地方计算出来;
按照这个要求,我们可以进行筛选:
- 原始的产品列表是由 props 传递的,所以它不是状态;
- 用户输入的搜索文本会随着时间改变(用户改变),并且无法根据任何内容进行计算;
- 单选框的值会随着时间改变(用户改变),并且无法根据任何内容进行计算;
- 经过筛选的产品列表可以由原始的产品列表、单选框的值、输入的搜索文本计算出来(筛选),因此它也不是状态;
所以,只有搜索文本和单选框的值才是状态,Nice Done!
React 中有两种类型的“模型”数据:props 和 state。两者区别很大:
props 就像传递给函数的参数。它们允许父组件将数据传递给子组件并自定义其外观。例如,Form 可以将颜色 props 传递给 Button。
state 就像一个组件的内存。它允许组件跟踪一些信息,并根据交互对其进行更改。例如,按钮可能会跟踪 isHovered 状态。
props 和 state 是不同的,但它们是协同工作的。父组件通常会将一些信息保持在 state (可以进行改变),并将其作为子组件的 state 传递给子组件。如果第一次阅读时仍然感觉不清楚,那也没关系。它需要一点练习才能真正坚持下来!
决定 State 的位置
在确定了应用的状态后,还需要确定哪个组件负责拥有和更改这些状态。记住:React使用单向数据流,数据自顶向下流动,数据从父组件向下传递到子组件。目前可能还不清楚哪个组件应该拥有什么状态,如果你是这个概念的新手,这可能很有挑战性,但可以按照以下步骤来解决:
对于应用中的每个状态:
-
识别基于该状态呈现某些内容的每个组件;
-
找到它们最接近的公共父组件————在层次结构中位于它们之上的组件。
-
决定这个组件所在的位置:
通常,可以将状态直接放入它们的公共父级中,也可以将状态放入其公共父级之上的某个组件中。
如果找不到拥有状态的组件,请创建一个仅用于保存状态的新组件,并将其添加到公共父组件上方层次结构中的某个位置。
现在回到这个例子中的状态,决定它们的位置:
识别使用状态的组件:
-
ProductTable 需要根据该状态(搜索文本和单选框的值)筛选产品列表。
-
SearchBar 需要显示该状态(搜索文本和单选框的值)。
找到它们的共同父组件:两个组件共享的第一个父组件是 FilterableProductTable。
决定状态的位置:可以在 FilterableProductTable 中保存搜索文本和单选框的状态值。
因此,状态值将存在于 FilterableProductTable 中,代码如下:
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = React.useState('');
const [inStockOnly, setInStockOnly] = React.useState(false);
这里需要提一下,useState
方法只在 React 16.8 版本以上存在,由于我之前使用的是 16.4 版本,这地方一直报错方法不存在,研究了半天才发现问题。
接着,将 State filterText 和 inStockOnly 作为 props 传递给 ProductTable 和 SearchBar:
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>
);
}
当 filterText 或 inStockOnly 状态改变时(用户进行了操作),拥有这些状态的组件 FilterableProductTable 会感知到状态的变化,重新渲染页面内容;又因为 ProductTable 和 SearchBar 组件在 FilterableProductTable 组件中,同时接收了这两个状态作为 props,它们也会重新渲染。这样,ProductTable 和 SearchBar 就可以根据状态更改自身显示的内容了。
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>
);
}
其中,SearchBar 组件根据这两个状态,更改页面上显示的内容;ProductTable 组件根据这两个状态,重新计算经过筛选的产品列表,如此便实现了页面的动态变化。
添加反向数据流
目前,应用已经通过自顶向下流动的 props 和 state 进行了正确的渲染。但是,要想根据用户输入更改状态,还需要支持数据以另一种方式流动:位于层次结构中下方的表单组件需要更新 FilterableProductTable 中的状态。
React明确地规定了数据流动的方式,但相比于双向数据绑定,它需要更多的输入(代码编写)。如果尝试在上面的组件构成的页面中输入搜索内容或改变单选框,可以看到输入没有响应,被 React 忽略了。这是故意的(😓)。由于代码中的 <input value={filterText}/>
,搜索框的值始终等于从 FilterableProductTable 传入的 filterText 状态。由于 filterText 状态初始为空,且从未被 setStatu
方法设置(只有这个方法才能改变状态,React 也是从这感知到了状态的变化),因此搜索框的值永远不会更改(单选框同理)。
我们需要的效果是,当用户更改表单输入时,状态都会更新并反映这些更改。该状态由 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} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>
);
}
可以理解为,应用已经完成了自顶向下的数据流动,但用户位于底层,用户的操作需要自底向上传递给应用。通过将改变状态的函数作为引用传递给 SearchBar,它也拥有了改变状态的能力,因此,SearchBar -> FilterableProductTable 的流动被打通了。而用户与 SearchBar 之间的流动,就是简单的事件响应了,在 SearchBar 中添加如下事件:
<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>
这样,用户操作 -> SearchBar 的流向也被打通了。因此,用户操作 -> SearchBar -> FilterableProductTable 的自底向上的流向被打通了:
//import useState from 'react';
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = React.useState('');
const [inStockOnly, setInStockOnly] = React.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"}
];
function App() {
return <FilterableProductTable products={PRODUCTS} />;
}
对页面操作的声音从山底传向了山顶,虽然微不足道,但已足以引发山顶的数据瀑布倾盆而下。
最后放两个和 GPT 的对话:
Q:onFilterTextChange={setFilterText} 可以理解为将 setFilterText 函数赋给 onFilterTextChange,这样调用 onFilterTextChange 就相当于调用 setFilterText 了,对吗
A:是的,完全正确。在这里,onFilterTextChange被设置为setFilterText函数的引用,因此当调用onFilterTextChange时,它实际上会调用setFilterText。这个技巧在React中非常常见,因为它允许将状态更新的责任从子组件委托给父组件,从而使代码更清晰和模块化。
Q:onFilterTextChange={setFilterText} 修改了 FilterableProductTable 组件的状态更新,它会重新渲染其中的所有子组件是吗
A:是的,当 FilterableProductTable 组件的状态更新时,它会重新渲染其所有的子组件,包括 SearchBar 和 ProductTable 组件。因为 SearchBar 和 ProductTable 组件的 props 都是由 FilterableProductTable 组件的状态所控制的,所以当 FilterableProductTable 组件的状态更新时,会重新传递新的 props 给其子组件,从而触发它们的重新渲染。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?