useState是React提供的Hook中最基本的一个Hook,它可以让你在函数组件中使用状态变量。本文将详细讲解useState Hook的基本概念,使用方法和应用技巧。让你能够更好地利用useState Hook进行状态管理。
useState基本介绍
useState接受一个初始状态作为参数,返回一个包含当前状态值和一个更新状态的函数的数组。
【定义方式】
// 这里可以任意命名,因为返回的是数组,数组解构
const [state, setState] = useState(initialState);
【例如】
// 声明一个名为count的状态变量,初始值为0
const [count, setCount] = useState(0);
Tip:
可以通过调用更新状态的函数【setState】来改变状态值,并触发组件重新渲染。
可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接替换
set函数
调用 set 函数 不会 改变已经执行的代码中当前的 state,即调用 set 函数不能改变运行中代码的状态。
这是什么意思呢?
比如现在定义了一个状态是
const [count, setCount] = useState(0);
然后通过定义一个事件来改变它的值
function handleClick() {
console.log(count); // 0
setCount(count + 1); // 请求使用 1 重新渲染
console.log(count); // 仍然是 0!
setTimeout(() => {
console.log(count); // 还是 0!
}, 5000);
}
运行代码,通过打印会发现,这个事件执行之后,会改变界面上值的渲染,但是如果在这个事件中直接打印count,不论通过什么方式,打印出来的值都是原来得的值。
构建state的原则
当你编写一个存有 state 的组件时,你需要选择使用多少个 state 变量。那要怎么构建出良好的state呢,可以参考以下几个原则:
合并关联的 state
如果某两个 state 变量总是一起变化,则将它们统一成一个 state 变量。
比如:
// ⭕️ bad
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// ✅ good
const [position, setPosition] = useState({ x: 0, y: 0 });
避免矛盾的 state
如果多个state之间始终是互斥的,则可以选择使用一个状态来代替,例如:
// ⭕️ bad
const [text, setText] = useState('');
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsSending(true);
await sendMessage(text);
setIsSending(false);
setIsSent(true);
}
if (isSent) {
return <h1>Thanks for feedback!</h1>
}
以上示例中,setIsSending-发送中,setIsSent-已发送,这两个状态是永远不可能为true的,所以可以改用一个 status 变量来代替它们,这个 state 变量可以采取三种有效状态其中之一:'typing' (初始), 'sending', 和 'sent':
// ✅ good
const [text, setText] = useState('');
const [status, setStatus] = useState('typing');
async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}
const isSending = status === 'sending';
const isSent = status === 'sent';
if (isSent) {
return <h1>Thanks for feedback!</h1>
}
避免冗余的 state
如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中。
示例:
一个关于计算的常见示例
// ⭕️️ bad
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}
示例中有三个 state 变量:firstName、lastName 和 fullName。然而,fullName 是多余的。在渲染期间,始终可以从 firstName 和 lastName 中计算出 fullName,因此需要把它从 state 中删除。
// ✅ good
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName;
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
为什么,如果能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中?
-
避免不必要的计算和内存消耗。
在React中,组件的state用于存储与组件的可变性相关的数据。当state发生变化时,React会重新渲染组件,以确保UI与最新的数据保持同步。然而,在渲染期间,React会执行大量的优化操作,其中包括对组件的props和state进行比较,以确定是否需要重新渲染。 假设我们将计算出的信息存储在组件的state中。每当组件的props或state发生变化时,React会重新计算这些信息,并将其存储在state中。这会导致不必要的计算和内存消耗。
-
代码维护性问题
我们在组件中添加更多的状态变量时,我们需要更多地关注它们之间的依赖关系和状态更新的逻辑。这可能导致代码变得难以维护和理解。
示例:
不要在 state 中镜像 props
// ⭕️ bad
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
}
// ✅ good
function Message({ messageColor }) {
const color = messageColor;
}
bad写法存在一个问题,它会失去与父组件的同步,因为state 仅在第一次渲染期间初始化。
如果父组件稍后传递不同的 messageColor 值(例如,将其从 'blue' 更改为 'red'),则 color state 变量将不会更新!
但是这种写法也不算是错误,但你只需要props的初始值时,你可以这样写。
function Message({ initialColor }) { // 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。 // 对于 `initialColor` 属性的进一步更改将被忽略。 const [color, setColor] = useState(initialColor); }
删除不必要的state变量
例如,现在有一个表单,它的所有状态如下
【图取官网】
现在先试着列出所有的 state,确保所有可能的视图状态都囊括其中:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
现在我们可以来优化这些 state 变量,要怎么优化呢,可以根据这几个问题来判断是否需要这state
-
这个 state 是否会导致矛盾?
例如,isTyping 与 isSubmitting 的状态不能同时为 true。
矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,可以将 'typing'、'submitting' 以及 'success' 这三个中的其中一个与 status 结合。
-
相同的信息是否已经在另一个 state 变量中存在?
例如:isEmpty 和 isTyping 不能同时为 true。
通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,你可以移除 isEmpty 转而用 message.length === 0。
-
你是否可以通过另一个 state 变量的相反值得到相同的信息?
isError 是多余的,因为你可以检查 error !== null。
在清理之后,现在只剩下 3 个(从原本的 7 个!)必要的 state 变量
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
组件状态共享
有的时候会希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”
折叠面板是一个比较常见的组件,可以通过折叠面板来收纳内容区域
示例1)
实现一个折叠面板,可同时展开多个面板,面板之间不影响
import { useState } from 'react';
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
显示
</button>
)}
</section>
);
}
export default function Accordion() {
return (
<>
<Panel title="关于">
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
</Panel>
<Panel title="词源">
这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
</Panel>
</>
);
}
在这个例子中,父组件 Accordion 渲染了 2 个独立的 Panel 组件。每个 Panel 组件都有一个布尔值 isActive,用于控制其内容是否可见。点击其中一个面板中的按钮并不会影响另外一个,他们是独立的。
示例2)
实现手风琴效果,每次只能展开一个面板
import { useState } from 'react';
function Panel({
title,
children,
isActive, // 把控制权交给父组件,通过父组件的props传值来控制isActive
onShow // 向下传递事件处理函数,让子组件可以修改父组件的状态
}) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>
显示
</button>
)}
</section>
);
}
export default function Accordion() {
// 一次只能激活一个面板。所以Accordion 这个父组件需要记录哪个面板是被激活的面板
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Panel
title="关于"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
</Panel>
<Panel
title="词源"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。
</Panel>
</>
);
}
受控组件和非受控组件
- “受控”(由 prop 驱动),当组件中的重要信息是由 props 而不是其自身状态驱动时,就可以认为该组件是“受控组件”,它允许父组件完全指定其行为,例如示例2
- “不受控”(由 state 驱动),如示例1中,带有 isActive 状态变量的 Panel 组件就是不受控制的,因为其父组件无法控制面板的激活状态。
小技巧
使用惰性初始值
不要把只需要计算一次的的东西直接放在函数组件内部顶层 block 中。
我们通常会给useState一个初始值,比如
// ✅ 这样写不会有什么性能问题
const [count, setCount] = useState(0);
但是实际上,我们的初始值有可能是通过复杂的计算得到的,比如
// ❌ Bad,会产生性能问题
const initalState = heavyCompute(() => { /* 这里做很多计算*/});
const [count, setCount] = useState(initalState);
上面这段代码有什么问题呢?
首先要知道,组件每次useState的时候组件都会重新渲染。那么 initalState 在每次 render 时都会被重新计算,这无疑会造成严重的性能问题。
而对于 useState 的初始值,只需要计算一次即可,这个时候就可以通过 useState 的惰性初始值来解决这个问题。
// ✅ 这样初始值就只会被计算一次了
const [state,useState] = useState(() => heavyCompute(() => { /* 这里做很多计算 */}););
使用更新函数
现在有这样一个state和点击事件,在点击事件中调用了3次更新,它的结果会是什么呢?
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
我们期望的是,点击+3的时候number递增三次,但是运行之后,会发现,无论你调用多少次 setNumber(number + 1)
,每次点击的结果都是加1。
这是因为设置 state 只会为下一次渲染变更 state 的值。比如在第一次渲染期间,number
为 0
。即便在调用了 setNumber(number + 1)
之后,本次number
的值也仍然是 0
,所以一直都是
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
【问题解决】
那如果要在一个事件中多次更新某些 state应该怎么办呢?
可以使用
setNumber(n => n + 1)
更新函数来处理
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
// 这样写,每次点击就会+3
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
// 设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
// 这个时候第一点击,显示的是3,但是如果在这里直接打印number,结果依然是0
console.log('number: ', number);
}}>+3</button>
</>
)
}
在这里,
n => n + 1
被称为 更新函数。当你将它传递给一个 state 设置函数时:
- React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。
- 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。
【命名惯例】
通常可以通过相应 state 变量的第一个字母来命名更新函数的参数
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
🎨【点赞】【关注】不迷路,更多前端干货等你解锁
往期推荐