使用 React 时最常见的错误

为初始状态设置不正确的值类型
一个常见的错误是将对象或数组的初始状态值设置为null空字符串,然后在渲染期间尝试访问该值的属性。同样,不为嵌套对象提供默认值并尝试在渲染方法或其他组件方法中访问它们可能会导致问题。

考虑一个UserProfile我们从 API 获取用户数据并显示包含用户名的问候语的组件。

jsx
复制
import { useEffect, useState } from “react”;

export function UserProfile() {
const [user, setUser] = useState(null);

useEffect(() => {
fetch(“/api/profile”).then((data) => {
setUser(data);
});
}, []);

return (


Welcome, {user.name}


);
}
尝试呈现此组件将导致错误:无法读取 null 的属性“名称”。

将初始状态值设置为空数组然后尝试从中访问第 n 项时,会出现类似的问题。当通过 API 调用获取数据时,组件会呈现提供的初始状态。null尝试访问或元素的属性undefined将导致错误。因此,确保初始状态紧密代表更新后的状态至关重要。在我们的示例中,正确的状态初始化应该是:

jsx
复制
import { useEffect, useState } from “react”;

export function UserProfile() {
const [user, setUser] = useState({ name: “” });

useEffect(() => {
fetch(“/api/profile”).then((data) => {
setUser(data);
});
}, []);

return (


Welcome, {user.name}
// Renders without errors

);
}
这种方法适用于上面的简单示例。但是,为状态值提供默认值可能并不总是实用的,尤其是在处理大型对象时。在这种情况下,可以推迟组件的渲染,直到数据被获取。实现此目的的一种方法是使用加载状态初始化组件并显示加载器,直到数据被获取并准备好呈现(或直到数据获取期间发生错误)。

jsx
复制
import { useEffect, useState } from “react”;

export function UserProfile() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
fetch(“/api/profile”)
.then((data) => {
setUser(data);
})
.catch((error) => {
// Handle the error
})
.finally(() => {
setIsLoading(false);
});
}, []);

if (isLoading) {
return

Loading…
;
}

return (


Welcome, {user?.name}


);
}
我们还使用一个可选的链接运算符来防止在返回的数据是null. 但是,仍然需要额外的错误处理,因为在这种情况下,组件将显示没有名称的问候语。

请注意,我们在方法中重置了加载状态finally,无论获取操作是否成功,它都会将其删除。这确保加载状态总是在异步操作完成后重置。

如果您正在寻找有关如何成为自学成才的开发人员的一些指导,您可能会发现这篇文章很有帮助:关于成为自学成才的开发人员的提示。

直接修改状态
React 的状态被认为是不可变的,不应该直接改变。相反,使用钩子中的 setter 函数useState。此外,在更新非原始值(例如存储在状态中的对象或数组)时,我们需要为它们提供不同的对象引用(也称为指针)。否则,React 将无法识别状态已更改并且不会更新 UI。

使用简单数组
考虑一个例子,我们有一个可以添加新项目的待办事项列表。该AddItem组件允许用户为待办事项输入文本并保存。

jsx
复制
const TodoList = () => {
const [items, setItems] = useState([]);

const onItemAdd = (item) => {
items.push(item);
setItems(items);
};

return (


{items.length > 0 ? (

Your todo items

  • {items.map((item) => (
  • {item}

  • ))}


) : (
No items

)}


);
};
在此示例中,当我们键入一个新项目并保存它时,它不会出现在待办事项列表中。出现此问题是因为Array#push改变了原始数组。当我们将它保存到状态时,它仍然指向与之前相同的内存位置,因此 React 无法识别任何更改。

items要解决这个问题,我们需要创建一个新的数组实例并添加新项,而不是通过items添加新项来修改数组。这可以使用传播语法来实现。

jsx
复制
const TodoList = () => {
const [items, setItems] = useState([]);

const onItemAdd = (item) => {
setItems([…items, item]);
};

return (


{items.length > 0 ? (

Your todo items

  • {items.map((item) => (
  • {item}

  • ))}


) : (
No items

)}


);
};
现在,当我们添加一个新项目时,它会正确显示在 UI 中。这说明了在更新状态值时使用不可变对象操作方法的重要性。

对象数组的不可变更新
当只包含字符串或数字值时,创建新数组相对简单。但是更复杂的情况呢,比如对象数组?

考虑一个示例,我们需要checked根据复选框的状态更新数组中特定对象的字段。

javascript
复制
const updateItemList = (event, index) => {
itemList[index].checked = event.target.checked;
setItemList(itemList);
};
这种方法同样行不通,因为我们直接修改了数组listFeatures。我们不能像前面的例子一样使用展开语法。但是,我们仍然可以使用它来创建原始数组的副本并修改该副本。

javascript
复制
const updateItemList = (event, index) => {
const newList = […itemList];
newList[index].checked = event.target.checked;

setItemList(newList);
};
这种方法之所以有效,是因为我们创建了一个新数组,即使我们直接在特定索引处改变其元素,新的数组引用也足以让 React 检测到更改并使用新列表更新 UI。

但是,这种方法会产生另一个问题。当使用扩展语法(类似于concat或slice方法)创建数组副本时,数组内的值仅复制一层深,称为浅副本。假设我们的items数组有多层嵌套的对象,并且这个items数组也在应用程序的其他部分使用(例如,我们存储items[0]为选中项)。如果我们在这个浅拷贝中修改一个对象的属性,它会影响应用程序中这个对象的所有其他实例,即使我们没有显式更新它们。

为避免此类难以调试的问题,最好在修改状态时使用适当的不变性方法。根据对象深度,我们可以使用该map方法创建一个新数组及其中的对象。

javascript
复制
const updateFeaturesList = (event, index) => {
const newFeatures = features.map((feature, idx) => {
if (idx === index) {
return {…feature, checked: event.target.checked}
}
return {…feature};
});

setListFeatures(newFeatures);
};
通过使用map对象传播,我们确保原始状态项保持不变。

items另一种方法是使用structuredClone API对数组进行深度复制,现在所有主流浏览器都支持这种方法。

javascript
复制
const updateItemList = (event, index) => {
const newList = structuredClone(itemList);
newList[index].checked = event.target.checked;

setItemList(newList);
};
structuredClone创建值的深拷贝,允许我们安全地改变新数组。

虽然这两种方法都提供了不可变更新状态的方法,但它们在速度和内存使用方面可能不是最佳的,尤其是在数据量很大的情况下。在这种情况下,请考虑使用专门的库来处理不可变数据,例如Immer.js。

按值传递与按引用传递
了解按值传递和按引用传递之间的差异对于优化 React 中的组件重新渲染和状态管理至关重要。需要注意的是,JavaScript 并不真正支持按引用传递,所有数据都是按值传递的。

通常所说的“按引用传递”实际上是对象引用或内存地址指针。当一个对象引用被传递给一个函数时,该函数接收到对象内存地址的副本,这意味着两个不同堆栈帧上的两个不同变量指向相同的内存位置。这允许函数修改对象的属性,但它不能使原始引用指向新对象,这就是“通过引用传递”在真正支持它的语言中的含义,如 C++。要更深入地研究这个主题,有一篇很棒的文章:JavaScript 是按引用传递吗?.

了解使用对象引用的含义在 React 中尤为重要,因为它在更新组件状态或道具时在内部依赖于相同的值相等。React 使用内存地址来确定 state 或 props 中的对象或数组是否已更改,以及是否需要重新渲染。这就是为什么采用不可变数据处理实践(例如在更新状态时创建新对象或数组)对于避免意外副作用和不必要的重新渲染至关重要。

忘记设置状态是异步的
使用 React 时最常见的错误之一是试图在设置状态值后立即访问它。例如,考虑以下代码片段:

javascript
复制
import { useState } from “react”;

function Counter() {
const [count, setCount] = useState(0);

const increment = () => {
setCount(count + 1);
console.log(count); // 0
};
}
设置新值时,不会立即发生。通常,它在下一个可用的渲染器上执行或批处理以优化性能。因此,在设置状态值后访问状态值可能不会反映最新的更新。在基于类的 React 组件中,这个问题可以通过使用可选的第二个参数 for 来解决setState,这是一个回调函数,在状态更新为最新值后调用。

javascript
复制
increment = () => {
this.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count); // Updated state value
});
};
但是,钩子的工作方式不同,因为 setter 函数 fromuseState没有类似于setState. 过去,建议使用useEffect钩子来访问更改后的状态值。

javascript
复制
import { useEffect, useState } from “react”;

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(count); // Will be called when the value of count changes
}, [count]);

const increment = () => {
setCount(count + 1);
};
}
在 React 18 中,建议减少useEffect组件内部的调用次数。对于这种特定情况,更好的方法是在事件处理程序中执行计算。

javascript
复制
import { useState } from “react”;

function Counter() {
const [count, setCount] = useState(0);

const increment = () => {
setCount(count + 1);
console.log(count + 1);
};
}
重要的是要注意设置状态不是以返回承诺的方式异步的。所以,添加async/await或使用它then是行不通的,这是另一个常见的错误。

错误地依赖当前状态值来计算下一个状态
这个问题与上面讨论的问题密切相关,尽管它发生的频率较低。看看下面的代码片段:

javascript
复制
const [count, setCount] = useState(0);

const incrementTwice = () => {
setCount(count + 1);
setCount(count + 1);
};
count执行此函数后,我们可能期望的状态值为2。然而,它实际上是1。这是因为 React 会尽可能地批处理状态更新,以防止不必要的重新渲染。虽然在这个例子中被调用了两次,但两次调用期间setCount的值仍然是 ,所以无论调用多少次,最终结果总是。count01setCount

处理这种情况的正确方法是使用setCount.

javascript
复制
const [count, setCount] = useState(0);

const increment = () => {
setCount(c => c + 1);
setCount(c => c + 1);
};
在这种情况下,React 将始终使用的最新值count并正确更新它。React 文档中也对此进行了更详细的讨论。

在挂钩依赖数组中包含非原始对象或值
另一个常见错误是在钩子的依赖数组中包含对象、数组或其他非原始值。考虑以下代码片段:

javascript
复制
function FeatureList() {
const defaultFeatures = [“feature1”, “feature2”];

const isDefault = useCallback(
(feature) => defaultFeatures.includes(feature),
[defaultFeatures]
);
}
在这个例子中,我们想要记忆isDefault函数以防止它在每次渲染时被计算;但是,此代码存在问题。

当我们将数组作为依赖项传递时,React 仅存储其对象引用(内存地址指针)并将其与数组的先前对象引用进行比较。但是,由于features数组是在组件内部声明的,因此在每次渲染时都会重新创建它。这意味着它的引用每次都是新的,并且不等于 跟踪的引用useCallback。结果,回调函数将在每次渲染时运行,即使数组没有改变。

解决此问题的一种方法是将defaultFeatures数组包裹在useMemo钩子中。

javascript
复制
const defaultFeatures = useMemo(() => [“feature1”, “feature2”], []);
然而,在大多数情况下,这种方法是矫枉过正的,很容易导致依赖管理失控。比如ifdefaultFeatures本身依赖于一些其他的变量等等。

在这种情况下,更好的解决方案是简单地将变量声明移到组件外部,这样它就不会在每次渲染时都重新创建。

javascript
复制
const defaultFeatures = [“feature1”, “feature2”];

function FeatureList() {
const isDefault = useCallback(
(feature) => defaultFeatures.includes(feature),
[]
);
}
过度使用 useCallback 和 useMemo
在前面的示例中,我们在钩子中包装了一个函数useCallback,以防止在每次渲染时重新计算其结果。看起来我们这样做是在提高应用程序的性能;然而,这并非总是如此。useCallback过度使用像和 这样的钩子useMemo会适得其反。用每个函数useCallback或每个变量包装useMemo会产生不必要的开销,实际上会降低性能。例如,如果依赖项是一个非常大的数组,则依赖项更改的计算可能比额外的渲染或函数调用花费更长的时间。

相反,我们应该只在必要时才使用这些钩子。例如,我们可以使用useCallbackmemoize 一个函数,该函数作为 prop 传递给子组件。同样,useMemo可用于缓存计算成本高昂的值。我们应该避免将这些钩子用于琐碎的函数和值。

只需 5.98 美元即可获得 .com
为每个输入添加单独的 onChange 处理程序
在 React 中构建表单时,通常使用onChange事件来更新组件的状态。可能导致代码臃肿的一个错误是onChange为每个输入字段添加单独的处理程序。这很快就会变得很麻烦,尤其是在处理具有许多输入的复杂表单时。

假设我们有这个ProfileForm组件。

jsx
复制
function ProfileForm() {
const [formData, setFormData] = useState({
firstName: “”,
lastName: “”,
email: “”,
});

const onFirstNameChange = (event) => {
setFormData({ …formData, firstName: event.target.value });
};

const onLastNameChange = (event) => {
setFormData({ …formData, lastName: event.target.value });
};

const onEmailChange = (event) => {
setFormData({ …formData, email: event.target.value });
};

return (





);
}
我们可以看到所有处理程序中唯一的可变部分onChange是字段名称。我们可以onChange通过创建柯里化函数然后使用字段名称调用它来使用单个处理程序:

jsx
复制
const onInputChange = (name) => (event) => {
setFormData({ …formData, [name]: event.target.value });
};

// …

<input
type=“text”
name=“firstName”
value={formData.firstName}
onChange={onInputChange(“firstName”)}
/>
但是,还有更好的方法。相反,我们可以使用单个onChange处理程序并根据name输入的属性及其值更新状态。

jsx
复制
function ProfileForm() {
const [formData, setFormData] = useState({
firstName: “”,
lastName: “”,
email: “”,
});

const onInputChange = (event) => {
const { name, value } = event.target;
setFormData({ …formData, [name]: value });
};

return (





);
}
这种方法使我们能够编写一个函数来处理所有输入更改,从而简化和维护了我们的代码。它不一定是属性name。如果您的输入元素已经有一个 assigned id,则可以改用它。

如果您对在 React 中使用表单感到好奇,您可能会发现这篇文章很有帮助:使用 React Hook 表单管理表单。

不必要地使用 useEffect
随着 React 18 中并发渲染的引入,正确使用 hooks 比以往任何时候都更加重要useEffect。不必要地使用useEffect会导致性能问题、副作用和难以调试的问题。

useEffect我们应该只在需要执行影响外部世界的副作用时使用,例如从服务器获取数据、订阅事件或更新 DOM。这些副作用通常是异步的,并且可能会不可预测地发生,因此有必要在我们的组件中管理它们。

然而,当我们只需要执行影响组件内部状态或不影响外部世界的副作用时,我们可以使用其他钩子或纯 JavaScript 而不是useEffect. 更新后的 React 文档提供了一个完整的列表useEffect,列出了可能不需要的情况以及如何替换它。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q shen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值