使用 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