使用 React Hooks 时要注意的事项总结
介绍
我想至少了解 React,但有些事情我没有做,所以
我把我觉得通俗易懂的文章总结到了这篇文章中作为参考。
小心你如何使用 useState
上面的文章很容易理解,我学到了很多!
对于每个项目,我将从参考文章中学到的部分拾起并写出来。
考虑对相关状态进行分组
一种常见的模式是考虑诸如登录信息之类的事情。
例如,您可以使用email
和password
作为一组登录。
所以它看起来像这样!
↓改写前
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const handleChangeEmail = (value: stirng) => {
setEmail(value)
}
const handleChangePassword = (value: string) => {
setPassowrd(value)
}
↓改写后
const [loginInfo, setLoginInfo] = useState({
email: "",
password: ""
})
const handleChangeEmail = (key: keyof typeof loginInfo, value: string) => {
setLoginInfo(prev => ({
...prev,
[key]: value,
}))
}
避免不一致的状态声明
如果您不断增加组件的状态,这很可能会无意中发生。
(即使在我参与的地狱项目中,也充满了矛盾的条件。)
参考文章中isSending
和isSent
由于在传输过程中和传输后不可能同时是true
,
不是维护单独的状态,而是像status: “SENDING” | “SENT”
这样的类型的状态
通过定义它会统一的感觉。
我经常在模态中这样定义useState
。这是因为我认为不会同时打开一个以上的模式。
例如:
↓改写前
const [modalA, setModalA] = useState(false)
const [modalB, setModalB] = useState(false)
const toggleModalA = () => {
setModalA(b => !b)
}
const toggleModalB = () => {
setModalB(b => !b)
}
↓改写后
const [selectedModal, setSelectedModal] = useState<"" | "modalA" | "modalB">("")
const toggleModalA = () => {
setSelected(prev => prev !== "modalA" ? "modalA" : "")
}
const toggleModalB = () => {
setSelected(prev => prev !== "modalB" ? "modalB" : "")
}
顺便说一句,您也可以像这样编写切换处理程序,
如果你把它变成一个接收参数的函数,
我不喜欢它,因为它在传递道具时会写成() ⇒ toggleModal(”modalA”)
。
const toggleModal = (value: "modalA" | "modalB") => {
setSelected(prev => prev !== value ? value : "")
}
不要使用多余的
这是参考文章的内容,除了firstName
和lastName
,
fullName
的状态不好。
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");
const handleChangeFirstName = (e) => {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName); // ←これが勿体無い
}
const handleChangeLastName = (e) => {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value); // ←これが勿体無い
}
虽然上面的描述可以从firstName
和lastName
计算出fullName
,
把它当成一个状态来保存是很浪费的,
同时更新firstName
和lastName
是一种浪费。
所以你可以像这样修复它:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
fullName = firstName + " " + lastName;
const handleChangeFirstName = (e) => {
setFirstName(e.target.value);
}
const handleChangeLastName = (e) => {
setLastName(e.target.value);
}
更重要的是,firstName
和 lastName
是相关信息,
就是它可以概括为userName
这样的一种状态。
总而言之,不是有一个可以从状态中的现有状态计算出来的值,
您的意思是将其定义为常数并在每次渲染时计算它?
文章中介绍的另一件事是,您还应该避免从 state 中的 props 传递值。
这意味着以下情况。
function Text({ children, color }) {
const [textColor] = useState(color);
return <h1 style={{ color: textColor }}>{children}</h1>;
}
export default function Example() {
const [color, setColor] = useState("red");
return (
<div>
<p>
色を選択
<select value={color} onChange={(e) => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
</p>
<Text color={color}>色が変わります</Text>
</div>
);
}
有了这个,useState 只在渲染时初始化,所以
textColor
即使改变了道具也不会改变,颜色也不会改变。
所以,像这样重写它:
function Text({ children, color }) {
const textColor = color;
return <h1 style={{ color: textColor }}>{children}</h1>;
}
这将更改 textColor
并在道具更改时正确更改颜色。
参考文章中给出了一个例子。
就个人而言,我认为不更改值的 useState 是不必要的,除非您不想与渲染同步更改值。
此外,如在这些示例中,如果它是在不使用 useState 的情况下随时重新计算的描述,
我认为计算量越大,就越应该关注考虑渲染的实现,例如考虑memoization。
避免重复的状态声明
请看下面的描述。
const initialItems = [
{ id: 1, title: "taskA" },
{ id: 2, title: "taskB" },
{ id: 3, title: "taskC" },
];
const [tasks, setTasks] = useState(initialItems);
const [selectedTask, setSelectedTask] = useState(tasks[0]);
function handleTaskChange(taskId, e) {
setTasks(tasks.map((task) => (task.id === taskId ? { ...task, title: e.target.value } : task)));
setSelectedTask((task) => (task.id === taskId ? { ...task, title: e.target.value } : task));
}
function handleSelectedTaskChange(task) {
setSelectedTask(task);
}
在此描述中,tasks
和 selectedTask
被声明为 useState
。
这个描述有什么问题?
task
和selectedTask
显然是一样的,所以useState
有两个定义。
这样在更新状态时,除了任务列表,
选中的任务也需要更新,所以我们要写两次同样的更新过程。
最终,这是多余的。
以下是对这些描述的一些改进:
const [tasks, setTasks] = useState(initialItems);
const [selectedTaskId, setSelectedTaskId] = useState(0);
function handleTaskChange(taskId, e) {
setTasks(tasks.map((task) => (task.id === taskId ? { ...task, title: e.target.value } : task)));
}
function handleSelectedTaskIdChange(taskId) {
setSelectedTaskId(taskId);
}
const selectedTask = tasks.find((task) => task.id === selectedTaskId);
简而言之,不是将选定的任务作为状态,
通过将选中任务的id
作为状态,可以解决更新时冗余的问题。
将所选任务描述为在渲染时重新计算的常数感觉很好。
这是一个非常简单的例子。如果要指定和处理原始数组信息中的任何一个,
很容易为使用useState
指定的信息创建新定义。
但是,在这种情况下,而不是指定的信息本身,
如果你有一个唯一的值将它作为一个状态指向它,你可以避免多余的描述。
无论如何使用 useCallback
上面的文章是绝对正确的!我强硬。
我经常听到React.memo
或useMemo
或useCallback
,
性能优化太酷了,我想用它! !这将是大声笑
但是,除非有实际的等价物,否则我认为我不会使用它,
首先调用这些钩子是有开销的,
我认为应该在观察应用程序的性能下降后在调整性能时使用它。
在进入正题之前,我希望您参考以下文章等,用于React.memo
、useMemo
和useCallback
。
我将在本文中非常简单地解释它。
React.memo
:
当父组件渲染时,可以防止子组件重新渲染,除非传递给 parent->child 组件的 props 已更改。如果您不使用React.memo
,它将与父组件一起呈现。
useMemo
:
记住函数计算的结果。只要依赖数组不改变,它就会一直返回相同的值。
useCallback
:
记住函数本身。由于它抑制了不必要的函数实例创建并返回与前一个函数相同的值(a === b
的关系),
对于使用React.memo
将函数传递给组件很有用。
如果你没有使用useCallback
,说明props发生了变化,因为它是一个函数对象,
如果您不使用React.memo
,它也会重新渲染。
现在我明白了React.memo
,useMemo
,useCallback
,
我将总结断言“无论如何都使用 useCallback”的具体理由。
首先,当我谈到“无论如何都使用 useCallback”意味着什么时,我认为在创建自定义挂钩时应该将所有内容都包含在 useCallback
中。
创建自定义钩子的原因与创建普通函数的原因完全相同:职责分离和封装是。一旦隔离为自定义钩子,接口的内部应该在自定义钩子中完成。自定义钩子的用户不应该知道自定义钩子里面有什么,反之亦然。
我以为是真的。参考资料中也提到
根据您使用它的位置,您可能会说,“我在这里使用
React.memo
,所以让我们使用useCallback
。”“我们这里不需要
useCallback
,而且我们担心开销,所以我们不要使用它。”如果它与用户的便利性相匹配,例如
任何缺乏可重用性和独立性的东西都会成为一个组件。
这就提出了自定义钩子是否履行其职责的问题。
此外,考虑到使用
useCallback
的开销非常痛苦,这意味着您应该从一开始就使用useCallback
。React给同一个值赋予了逻辑意义,所以在返回一个意义相同的函数时,应该尽可能地返回一个与对象相同的函数
我对此也很满意。
首先,它们是否等价在 React 本身中具有重要意义。所以,弄清它们是否等价在逻辑上意义重大!
由于这些原因,我也得出结论,无论如何我都会使用 useCallback。
尽量不要使用 useEffect
此声明基于官方文档中的上述内容。
我将解释何时不需要
useEffect
。首先,它是一种在更改其他数据的同时更改数据时不需要
useEffect
的模式。以下是参考文章的示例。
↓改写前
function Form() { const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); // ? Avoid: redundant state and unnecessary Effect const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(firstName + ' ' + lastName); }, [firstName, lastName]); // ... }
↓改写后
function Form() { const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); // ✅ Good: calculated during rendering const fullName = firstName + ' ' + lastName; // ... }
说明一下,在重写之前,如果
firstName
或lastName
发生变化,useEffect
用于更新fullName
的状态。当然可以正确更新
fullName
的值,但是效率很低,不是吗?这是因为如果计算的值在渲染时存储为常数,就像重写后一样,没有问题。
当
useState
的值发生更改时,会发生重新计算,就像“小心如何使用 useState”部分中描述的一样。所以在状态下拥有一个可以通过计算得到的值是多余的,更不用说使用
useEffect
,这显然是不必要的,在某些情况下甚至会导致不必要的渲染。现在让我们考虑如果重新计算是一个繁重的过程,该怎么办。
假设我们有以下处理:
function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); // ? Avoid: redundant state and unnecessary Effect const [visibleTodos, setVisibleTodos] = useState([]); useEffect(() => { setVisibleTodos(getFilteredTodos(todos, filter)); }, [todos, filter]); // ... }
假设
getFilteredTodos
是轻进程,看描述,如前所述,useEffect
是不必要的,所以可以改写如下。function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); // ✅ This is fine if getFilteredTodos() is not slow. const visibleTodos = getFilteredTodos(todos, filter); // ... }
和以前一样,在渲染时重新计算就足够了。
那么
getFilteredTodos
重的时候怎么写如下。import { useMemo, useState } from 'react'; function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); const visibleTodos = useMemo(() => { // ✅ Does not re-run unless todos or filter change return getFilteredTodos(todos, filter); }, [todos, filter]); // ... }
如上所述,如果使用memoization,则依赖数组(这里为
todos
和filter
)的值不会被重新计算,而之前的值会被重用。因此,即使由于多次重新计算繁重的处理而导致性能下降,您也可以使用
useMemo
解决它。 (“无论如何都使用 useCallback”中简要介绍了记忆化)下一个
useEffect
模式是一个模式,用于重置道具更改的状态。请先阅读以下说明。
export default function ProfilePage({ userId }) { const [comment, setComment] = useState(''); // ? Avoid: Resetting state on prop change in an Effect useEffect(() => { setComment(''); }, [userId]); // ... }
在这里,每个用户的个人资料通过
userId
的道具进行区分。还假设值
comment
允许您评论与userId
对应的用户的个人资料。在这种情况下,当
userId
变成别的东西时,存储在与先前userId
对应的用户的配置文件中输入的comment
的值中的值必须被重置。因此,如果
userId
更改为useEffect
,则comment
的值将重置为空字符串。如果没有要重置的
useEffect
,React 中相同组件的状态并在同一个地方渲染会继续保持,所以comment
的值之前在用户的配置文件中输入。它将保持。但即使在这种情况下,您也可以在不使用 useEffect 的情况下处理它。
为此,请像这样重写它:
export default function ProfilePage({ userId }) { return ( <Profile userId={userId} key={userId} /> ); } function Profile({ userId }) { // ✅ This and any other state below will reset on key change automatically const [comment, setComment] = useState(''); // ... }
对于奇怪的部分,我们将有状态的部分剪掉,指定
userId
作为处理的key。之所以能这样处理,是因为前面提到的“将 React 同一个组件的状态和被渲染的组件保持在同一个地方”的特性。
状态没有被重置,因为它被 React 首先识别为相同的东西,所以你告诉 React 这是不同的。
为此,指定一个唯一的密钥使这种对应成为可能。
React 中的键在用于
map
之类的东西时起着特殊的作用。如果你能看到下面的文章详细了解,我想你可以加深对关键的理解。
暂时这里简单解释一下,关键是React要关联item。
键必须是唯一值,并且与项具有一对一的关系。
本质上类似于
{ key: value }
的对象。所以一旦回到主题,我发现在上述情况下使用 key 消除了使用
useEffect
的需要。那么如果我们只想重置两个或多个状态中的一个呢?
如果您使用以前的方法,您将重置所有状态。
例如,假设我们有以下语句:
function List({ items }) { const [isReverse, setIsReverse] = useState(false); const [selection, setSelection] = useState(null); // ? Avoid: Adjusting state on prop change in an Effect useEffect(() => { setSelection(null); }, [items]); // ... }
这样,它可以采取随着项目变化而重置选择值的形式。
当然,这也有问题。问题显然是额外的渲染。
具体来说,当
items
改变时,渲染一次,然后当useEffect
中的setSelection(null)
重置selection
时,再次渲染。所以让我们像这样重写它:
function List({ items }) { const [isReverse, setIsReverse] = useState(false); const [selection, setSelection] = useState(null); // Better: Adjust the state while rendering const [prevItems, setPrevItems] = useState(items); if (items !== prevItems) { setPrevItems(items); setSelection(null); } // ... }
通过将先前的值作为状态并按上述方式处理,
items
和之前一样的变化导致了两个变化。但是,由于这在可读性方面很微妙,因此似乎有更好的方法。
例如,如果我们将其更改为:
function List({ items }) { const [isReverse, setIsReverse] = useState(false); const [selectedId, setSelectedId] = useState(null); // ✅ Best: Calculate everything during rendering const selection = items.find(item => item.id === selectedId) ?? null; // ... }
通过将
id
作为状态,您可以将呈现范围缩小到仅指向所选元素的值发生变化的时间。我想介绍的下一个模式是使用
useEffect
来初始化应用程序。例如,如果我们有以下描述:
function App() { // ? Avoid: Effects with logic that should only ever run once useEffect(() => { loadDataFromLocalStorage(); checkAuthToken(); }, []); // ... }
这仅在第一次渲染时加载本地存储并执行检查令牌的函数。
但是,这个
useEffect
也可以减少。可以通过如下重写来实现。
if (typeof window !== 'undefined') { // Check if we're running in the browser. // ✅ Only runs once per app load checkAuthToken(); loadDataFromLocalStorage(); } function App() { // ... }
通过将其写在组件外部而不是像上面那样写在组件内部,您可以使其成为读取文件的一次性执行,并且您可以在不写
useEffect
的情况下很好地执行它。此外,使用 React 18,
useEffect
在开发环境中会执行两次,因此也可以防止由此导致的意外行为。概括
到目前为止,我已经以一种易于理解的方式解释了它,以及一篇关于 React Hooks 的非常易于理解的文章,但我认为如果你将每篇原创文章阅读一遍,你就会有更深入的理解,所以请看一下!
参考文章
原创声明:本文系作者授权爱码网发表,未经许可,不得转载;
原文地址:https://www.likecs.com/show-308631505.html