状态管理是React中的一个基本概念,这一篇文章我们就来了解下组件状态是如何存储和更新的,这些知识对于创建复杂应用极为重要,我们需要透彻理解。
理解状态钩子(Understanding the State Hook)
我们已学习了State的用法,这里我们需要了解三点:
一,React更新 State是异步的。
比如我们在设置State语句后立即查看State,会发现并没有马上更新的。如下面的代码,点击按钮在控制台输出"false",可见并没有立即更新为 true 。这是为了避免频繁重新渲染页面,在事件处理函数中,可能还有类似setName('kelemi')等其他语句,一般是等事件处理函数结束后,才一并重新渲染。
function App() {
const [isVisible, setVisible] = useState(false);
const handleClick = () => {
setVisible(true);
console.log(isVisible);
};
return (
<div>
<button onClick={handleClick}>Show</button>
</div>
);
}
export default App;
二,React的State实际上是存于组件外部的。
因为定义在组件里的变量只作用于该组件函数里,当重新渲染时,里面的变量将被重置,所以必须存在于外部,否则没法工作。当屏幕上长久不显示该组件时,存在于组件外部的state变量将被自动清除。
三,在组件的顶部使用State Hook。
如下,我们再定义了一个State Hook,内部命名为isApprove,而React实际是不管这个名字的,它记录的类似列表,它只知道有两个boolean值,分别为 false,true。在重新新渲染时,它根据顺序映射到组件内部的名称中,所以不能将State放在 if 语句、循环语句以及嵌套语句中,这样会破坏顺序。我们在使用中要将State定义在组件函数的开头处。
function App() {
const [isVisible, setVisible] = useState(false);
const [isApprove, setApprove] = useState(true);
...
}
选择State结构
首先要避免冗余,看下面的State Hook,我们定义了firstName和lastName,就没必要再定义一个名为fullName的State Hook,因为我们完全可以由firstName和lastName组成fullName。
function App() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = firstName + " " + lastName;
return <div>{fullName}</div>;
}
其次,我们可以将相关联的State组合在一起,比如我们可以将firstName和lastName组合起来形成person。
function App() {
const [person, setPerson] = useState({
firstName: "",
lastName: "",
});
const [isLoading, setLoading] = useState(false);
...
}
另外,我们不能让State Hook的结构层次太深,多层嵌套。比如下面这样就不好。尽量保持扁平结构。
...
const [person,setPerson] = useState({
firstName:'',
lastName:'',
contact:{
address:{
street:'',
}
}
})
...
小结下State结构的最佳实践如下。
BEST PRACTICES
Avoid redundant state variables.
Group related variables inside an object.
Avoid deeply nested structures.
保持组件纯度
什么是纯度?它是计算机科学的一个基本概念。纯的函数是指给它同样的输入,总是输出一样的结果。如果同样的输入在不同的时间点输出是不一样的,那该函数就是不纯的。
React围绕着这个概念进行,当提供的输入props是一样时,期望输出也是一样的,也就可以不用重新渲染。
那如何保持组件的纯度呢?
将更改放在渲染阶段之外!
我们来看一下这是什么意思?
我们有Message组件,代码Message.tsx:
let count = 0;
const Message = () => {
return <div>Message {count}</div>;
};
export default Message;
然后在App.tsx中使用3个Message组件:
import Message from "./Message";
function App() {
return (
<div>
<Message />
<Message />
<Message />
</div>
);
}
export default App;
输出的三个组件是一致的,说明组件是纯的,如下:
# 访问:http://localhost:5173
Message 0
Message 0
Message 0
但要是在 Message.tsx里的渲染代码中添加修改代码"count++",如下:
let count = 0;
const Message = () => {
count++;
return <div>Message {count}</div>;
...
输出的各个Message就不一样了,这样组件就不纯了。
# 访问:http://localhost:5173
Message 2
Message 4
Message 6
所以在组件内,不要修改已存在的对象,这样才能保持组件是纯的。另外注意,如果创建和更新对象均放在组件内部实现,这是不影响的。比如我们将count的初始化也放在Message组件里就可以。
const Message = () => {
let count = 0;
count++;
return <div>Message {count}</div>;
};
理解Strict模式
前面演示组件的示例中,你是否注意到不纯的显示是Message 2、Message 4 以及Message 6,而不是期望的1、2、3,你知道为什么吗?
这跟React的strict模式有关。main.tsx中,App组件包含在 React.StrictMode中,这是React中内置的一个组件,不显示具体内容,而用于发现一些潜在的问题,其中之一就是检查组件是否是纯的。
ReactDOM. createRoot ( document . getElementById("root") as HTMLElement) . render(
<React .StrictMode><!--注意看 StrictMode -->
<App />
«/React. StrictMode>
);
在开发模式下,StrictMode执行2次,第1次用于检查,第2次用于实际渲染,所以我们看到的是2、4、6。
为了更清楚看到这个过程,我们在APP组件只留一个Message组件,同时修改Message.tsx,通过在控制台打印出相关信息:
let count = 0;
const Message = () => {
console.log("Message called", count);
count++;
return <div>Message {count}</div>;
...
查看控制台,看到执行了2次,第2行灰色的表示是strict模式输出。
另外,需要说明的是,在React 18下,默认Strict模式是开启的,也就是执行二次,主要用于发现组件是否不纯等问题,比如我们希望是1,结果输出是2,我们就能知道该组件不纯。而且,只在开发环境中strict模式才生效,当我们部署在生产环境时,Strict是关闭的,也就是只执行一次。
更新对象
前面我们说过,关联的State可以组成对象,我们在App组件添加名为drink的State。对于State,我们要把它看成是不变的或只读的,不要尝试改变它,这样是不会正确工作的。代码里,我们在击点处理事件里,修改了drink,但在页面上我们点击按钮时,不会有任何变化,说明这是不工作的。
function App() {
const [drink, setDrink] = useState({
title: "Americano",
price: 5,
});
const handleClick = () => {
drink.price = 6;
setDrink(drink);
};
return (
<div>
{drink.price}
<button onClick={handleClick}>Click Me</button>
</div>
);
}
我们在handleClick中需要定义一个新对象newDrink,然后再调用setDrink,如下:
const handleClick = () => {
const newDrink = {
title: drink.title,
price: 6,
};
setDrink(newDrink);
};
当State对象有很多属性时,我们可以使用对象复制(3个省略号)来处理。
const handleClick = () => {
setDrink({ ...drink, price: 6 });
};
更新嵌套对象
我们看一个稍复杂的State Hook,customer有两个属性: name和address,其中address也是对象且有两属性city和zipCode。click事件的工作就是更改zipCode。我们的代码是先复制整个customer,然后需要再复制customer.address。如果不复制customer.address的话,新建的customer对象与原来的customer指向了同一个address,这样是不行的,setCustomer的新建customer不要与原有的customer有任何关联。
function App() {
const [customer, setCustomer] = useState({
name: "kelemi",
address: {
city: "San Francisco",
zipCode: 94111,
},
});
const handleClick = () => {
setCustomer({
...customer,
address: { ...customer.address, zipCode: 94112 },
});
};
...
我们看到,嵌套的StateHook更改会比较复杂。一般情况,尽量保持State Hook扁平。
更新数组
数组作为State Hook也类似,需要新的数组赋于。增删改如下。
function App() {
const [tags, setTags] = useState( ["happy", "cheerful"]);
const handleClick = () => {
//添加,不能在原来基础上使用tag.push(''),而是要先复制
setTags( [...tags, "exciting"]);
//删除
setTags(tags.filter((tag) => tag !== "happy"));
//更新
setTags(tags.map((tag) => (tag === "happy" ? "happiness" : tag)));
};
...
更新对象数组
上一节说过数组更新用map,对象数组也不例会。下面的代码有个bug列表,点击按钮修改id为1的bug的fixed为true.
function App() {
const [bugs, setBugs] = useState( [
{ id: 1, title: "Bug 1", fixed: false },
{ id: 2, title: "Bug 2", fixed: false },
]);
const handleClick = () => {
setBugs(bugs.map((bug) => (bug.id === 1 ? { ...bug, fixed: true } : bug)));
};
...
我们可视化上述的代码。B1和B2是原有数组元素,B1*是新建的,而B2则就是原有数组元素,我们不必全部新建所有数组元素,只需新建要更新的元素。
B1 B2
B1* B2
使用Immer简化更新逻辑
我们看到,更新数组和对象有些麻烦,我们可以使用Immer简化它。首先安装Immer.
npm i [email protected]
再使用immer简化更新逻辑。首先导入produce,然后在setBugs中将produce函数作为参数,注意produce函数的参数 draft,它表示原来的State,在这里就相当于已存在的tags的副本,然后就可以像普通Javascript一样修改这个副本,Immer会自动设置好修改后的State.
import produce from "immer";
function App() {
...
const handleClick = () => {
setBugs(
produce((draft) => {
const bug = draft.find((bug) => bug.id === 1);
if (bug) bug.fixed = true;
})
);
};
...
我们来验证下是否生效。
...
return (
<div>
{bugs.map((bug) => (
<p key={bug.id}>
{bug.title} {bug.fixed ? "Fixed" : "New"}
</p>
))}
<button onClick={handleClick}>Click Me</button>
</div>
);
...
当我们点击按钮时,Bug 1 就变成了 Fixed了,与我们期望的一致。
组件间共享State
想象一个电子商务网站,导航栏组件显示购物车货物的数量,而购物车组件列出具体的购物清单,显示,这两个组件需要共享State。
如何共享State呢?
查看组件树,NavBar和Cart组件有共同的父组件App,我们可以将购物车清单这个State提升到App组件,App组件再通props传给2个子组件,这样就实现了State共享。
看下代码实现。先创建NavBar组件(快捷键rafce可以方便生成模板)。NavBar组件传递cartItemsCount 的 props,用于指示购物里清单数量。
import React from "react";
interface Props {
cartItemsCount: number;
}
const NavBar = ({ cartItemsCount }: Props) => {
return <div>NavBar:{cartItemsCount}</div>;
};
export default NavBar;
再创建Cart组件。定义props两个属性,一个是购物车清单cartItems,另一个是清除购物车操作,清除操作也要升至由父组App组件处理。
import React from "react";
interface Props {
cartItems: string [];
onClear: () => void;
}
const Cart = ({ cartItems, onClear }: Props) => {
return (
<>
<div>Cart</div>
<ul>
{cartItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<button onClick={onClear}>Clear</button>
</>
);
};
export default Cart;
现来看App组件。很简单,一目了然。
import { useState } from "react";
import NavBar from "./NavBar";
import Cart from "./Cart";
function App() {
const [cartItems, setCartItems] = useState( ["Product1", "Product2"]);
return (
<div>
<NavBar cartItemsCount={cartItems.length} />
<Cart cartItems={cartItems} onClear={() => setCartItems( [])} />
</div>
);
}
export default App;
更新State- 练习1
我们有个State为game, 当我们点击按扭时,更改game的player的name.
function App() {
const [game, setGame] = useState({
id: 1,
player: {
name: "John",
},
});
...
答案:
...
const handleClick = () => {
setGame({ ...game, player: { ...game.player, name: "Bob" } });
};
...
我们也可以使用immer来简化逻辑,这里就不详写了。
更新State- 练习2
我们有个State是pizza,当我们点击按钮时,添加配料Cheese.
function App() {
const [pizza, setPizza] = useState({
name: "Spicy Pepperoni",
toppings: ["Mushroom"],
});
...
答案:
...
const handleClick = () => {
setPizza({ ...pizza, toppings: [...pizza.toppings, "Cheese"] });
};
...
更新State- 练习3
更新cart,当点击按钮时,将id为1的protuct的quantity增加1。
function App() {
const [cart, setCart] = useState({
discount: 1,
items: [
{ id: 1, title: "Product 1", quantity: 1 },
{ id: 2, title: "Product 2", quantity: 1 },
],
});
答案:
...
const handleClick = () => {
setCart({
...cart,
items: cart.items.map((item) =>
item.id === 1 ? { ...item, quantity: item.quantity + 1 } : item
),
});
};
...
练习:创建可扩展文本组件
我们创建一个可扩展文件组件,可以指定显示前几个字符,点'more'按钮可以查看全部,点'less'又可以恢复缩略显示。
代码:
新建的ExpandableText.tsx:
import React, { useState } from "react";
interface Props {
children: string;
maxChars?: number;
}
const ExpandableText = ({ children, maxChars = 100 }: Props) => {
const [isExpanded, setExpanded] = useState(false);
if (children.length <= maxChars) return <p>{children}</p>;
const text = isExpanded ? children : children.substring(0, maxChars);
return (
<p>
{text}...
<button onClick={() => setExpanded(!isExpanded)}>
{isExpanded ? "Less" : "More"}
</button>
</p>
);
};
export default ExpandableText;
App.tsx:
import ExpandableText from "./ExpandableText";
function App() {
return (
<div>
<ExpandableText>lorem100</ExpandableText>
</div>
);
}
export default App;
说明:
lorem会自动随机生成文件,100表示100个随机单词,键入时就会随机生成。maxChars是可选的,默认为100。
有人可能会好奇为什么在ExpandableText里没用将text作为StateHook,正如前面我们说到过的,fullName没有必要使用StateHook存储一样,它可能根据其他生成,text也是同样道理。存储在StateHook里的只是那些随时间会变化,而且变化会导致重新渲染的变量,在我们这个例子里,只有 isExpanded需要存在StateHook里。
效果如下:
小结
状 态管理是React中的一个基本概念,本文我们了解了组件状态的存储和更新相关知识。下一篇介绍表单的创建。
若后续有更多课程章节,请移步到这里看:mp.weixin.qq.com/s/29JIyr7n954oni165pPYfQ
社区声明
1、本站提供的一切软件、教程和内容信息仅限用于学习和研究目的
2、本站资源为用户分享 ,如有侵权请邮件与我们联系处理 敬请谅解!
3、本站信息来自网络,版权争议与本站无关。您必须在下载后的24小时之内,从您的电脑或手机中彻底删除上述内容