大家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
本文部分内容来自于 Akshay Kaushik 发表的文章《Common Mistakes When Using React useState Hook (With Code Examples)》,文章链接已经在文末给出。
1.什么是 React Hooks
React hooks 是 React 16.8 中添加的函数,其允许函数组件保存状态、管理生命周期事件,并利用以前仅在基于类的组件中可用的其他 React 功能。 Hooks 使得以更清晰、更直接的方式开发可重用和模块化代码变得更加容易。
比如下面代码使用 Hooks 来更新 state 状态:
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={incrementCount}>
Increment
</button>
</div>
);
};
export default Counter;
使用 Hook 相对于 Class 有以下好处:
- 可读性和简单性:与基于类的组件相比,Hooks 提供了更简单和更短的语法。 使用 Hooks 可以创建函数组件而无需类,从而使代码更易于阅读和理解。 同时,Hooks 通过消除 this 关键字、构造函数和生命周期函数等生成更好的代码。
- 代码可重用性:Hooks 通过允许跨多个组件提取和重用逻辑来提高代码重用性。 在不使用高阶组件或渲染属性的情况下,可以使用自定义 Hooks 隔离有状态行为并与之交互。
- 更有效的管理:通过使用 Hooks,开发者可以更精确地分组和管理相关代码。 要单独处理不同问题,例如:状态管理、副作用和上下文可以在单个组件内使用各种 Hooks。
- 防止与类相关的问题在:React 组件中使用类可能会导致函数绑定、生命周期方法的使用和性能改进方面出现不清楚的情况。 这些问题可以通过 Hooks 解决,它提供了一种更简单的处理状态和效果的方法。
- 增强性能: Hooks 可以更轻松地提高性能, 利用 useCallback 和 useMemo 等 Hooks 可以缓存函数和值从而减少额外渲染迭代的需要并增强组件性能。
本文重点介绍 useState 这个最经常用到的最经典的 Hooks,谈谈 useState 的常见误区。
2.useState 常见误区
2.1 不理解 useState 的工作原理
useState Hooks 返回一个包含 2 个元素的数组,第一个元素是当前状态值,第二个元素是设置状态的函数。
重要的是要了解 useState 不会自动合并对象状态更新而是完全覆盖状态。
import React, {useState} from 'react';
function MyComponent() {
const [state, setState] = useState({
name: 'John',
age: 20
});
const handleUpdate = () => {
// 这样会覆盖已经存在的 state
setState({
name: 'Sam'
});
}
return (
<div>
{/* 调用 handleUpdate 后会渲染'Sam' 而不是'John'*/}
<h1>{state.name}</h1>
<h2>{state.age}</h2>
<button onClick={handleUpdate}>Update Name</button>
</div>
);
}
要正确合并状态更新,开发者可以将设置函数传递给 setState:
setState(prevState => {
// prevState 是初始状态
return {...prevState, name: 'Sam'}
});
2.2 内联传递函数
将内联函数传递给事件处理程序或 Effect 很方便,但这样做会破坏 state 更新状态:
import {useState, useEffect} from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 闭包 Closures 阻止代码更新
document.addEventListener('click', () => setCount(count + 1));
}, []);
return <h1>{count}</h1>;
}
由于函数闭包是在每次渲染时创建,因此 count 永远不会正确更新。 所以,建议仅当函数不更新状态时才传递内联函数,也可以在外部或 useCallback() 中声明:
// 外部声明
function increment() {
setCount(count + 1);
}
// useCallback() 中声明
const increment = useCallback(() => {
setCount(count + 1);
}, []);
注意:React useCallback Hooks 是一个回调,接受要优化的组件和回调变量。 JavaScript 会记住该变量,并在每次渲染时创建它以保持不变 ,从而消除不必要地重新计算值的需要。
2.3 没有正确处理数组和对象
另一个错误是直接改变状态数组或对象:
const [tags, setTags] = useState(['react', 'javascript']);
const addTag = () => {
// 直接更新已经存在的数组
tags.push('nodejs');
}
return <div>{tags}</div>
由于 tags 数组是直接修改的,React 无法检测到这种变化,也不会正确触发重新渲染,可以在传递给 setter 函数之前创建数组、对象的新副本:
const addTag = () => {
setTags([...tags, 'nodejs']);
}
这里的 spread 语法 [...array] 是浅复制数组,从而可以让 React 检测到变化。关于获取对象副本的更深入的了解可以阅读我的另外一篇文章《 JS 克隆对象八种技术,为何少不了 StructuredClone?》
2.4 进行不必要的 useState 调用
无需将每个字段或数据点分离到其自己的状态 hooks 中,这样做可能会不必要地损害性能:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// 注意:这里 firstName、lastName没必要单独放在一个Hooks
return (
<>
<input
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
<input
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</>
);
}
相反,开发者可以将相关 state 分组为单个对象:
function Form() {
// 注意:这里将多个 hooks 作为一个对象
const [fields, setFields] = useState({
firstName: '',
lastName: ''
})
// 一次调用更新所有字段
const handleChange = (e) => {
setFields({
...fields,
[e.target.name]: e.target.value
})
}
return (
<>
<input
name="firstName"
value={fields.firstName}
onChange={handleChange}
/>
<input
name="lastName"
value={fields.lastName}
onChange={handleChange}
/>
</>
);
}
2.5 不正确的状态比较
处理对象或数组时,避免直接比较状态变化。
// 不正确,无法确定数组或者对象的变化
if (user !== newUser) {
// ...
// 正确的示例
if (user.id !== newUser.id) {
// ...
}
2.6 没有设置正确的初始状态
根据 props 初始化
可以根据 props 初始化而不是硬编码值,比如下面的示例:
// 硬编码
const [count, setCount] = useState(0);
// 根据 props 初始化 state
const [count, setCount] = useState(props.initialCount);
处理卸载的数据
可以在 useEffect 中获取数据,设置加载状态:
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(user => setUser(user))
}, []);
// 渲染之前检查是否为 null
return <div>{user ? user.name : 'Loading...'}</div>
这可以避免渲染 undefined 状态时出现错误。
指定 state 的数据类型
指定 state 的类型有助于避免出现问题:
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<{name: string} | null>(null);
// 使用 typescript 指定类型
以上示例可确保防止将无效的更新状态值传递给 setter 函数。
3.本文总结
本文主要和大家介绍使用 useState Hooks 的六个常见疑问。因为篇幅问题,关于 useState Hooks 只是做了一个简短的介绍,但是文末的参考资料以及个人主页提供了大量优秀文档以供学习,如果有兴趣可以自行阅读。如果大家有什么疑问欢迎在评论区留言。
参考资料
https://blog.akshaykaushik.eu.org/common-mistakes-when-using-react-usestate-hook
https://www.geeksforgeeks.org/why-to-use-react-hooks-instead-of-classes-in-reactjs/
https://daveceddia.com/usestate-hook-examples/
https://www.youtube.com/watch?v=P24EuJlVKb0