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

34人参与, 0条评论 登录后显示评论回复

你需要登录后才能评论 登录/ 注册