使用 React 时最常见的错误
该文章最初发布在我的个人博客上。
在 Stack Overflow 上回答 React 相关问题时,我注意到大家在使用这个库时遇到的问题主要分为几类。我决定写一些最常见的问题,并展示如何处理它们,希望能对 React 新手,以及所有正在努力理解其基本概念的人有所帮助。本文交替介绍了使用基于类的组件和使用 hooks 的函数式组件的陷阱。
1. 直接修改状态
React 中的状态被视为不可变的,因此不应直接更改。应该使用钩子setState
中的特殊方法和 setter 函数useState
。考虑以下示例,您希望checked
根据复选框的状态更新数组中特定对象的字段。
const updateFeaturesList = (e, idx) => {
listFeatures[idx].checked = e.target.checked;
setListFeatures(listFeatures);
};
这段代码的问题在于,由于状态更新使用的是相同的对象引用,因此不会触发重新渲染,因此状态的更改不会反映在 UI 中。不直接修改状态的另一个重要原因是,由于状态的异步特性,后续的状态更新可能会覆盖直接对状态进行的更新,从而导致一些难以规避的错误。在这种情况下,正确的做法是使用 useState 的 setter 方法。
const updateFeaturesList = (e, idx) => {
const { checked } = e.target;
setListFeatures(features => {
return features.map((feature, index) => {
if (idx === index) {
feature = { ...feature, checked };
}
return feature;
});
});
};
通过使用map
对象扩展,我们还确保不会改变原始状态项目。
2. 初始状态设置错误的值类型
将初始状态值设置为null
空字符串,然后在 render 中像访问对象一样访问该值的属性,这是一个相当常见的错误。同样,不为嵌套对象提供默认值,然后在 render 或其他组件方法中尝试访问它们也会导致同样的错误。
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = {
user: null
};
}
componentDidMount() {
fetch("/api/profile").then(data => {
this.setState({ user: data });
});
}
render() {
return (
<div>
<p>User name:</p>
<p>{this.state.user.name}</p> // Cannnot read property 'name' of null
</div>
);
}
}
将初始状态的值设置为空数组,然后尝试访问其中的第 n 个元素时,也会出现类似的错误。当通过 API 调用获取数据时,组件将使用提供的初始状态进行渲染,尝试访问元素的属性null
将undefined
导致错误。因此,确保初始状态与更新后的状态紧密相关非常重要。在我们的例子中,正确的状态初始化如下:
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = {
user: {
name: ""
// Define other fields as well
}
};
}
componentDidMount() {
fetch("/api/profile").then(data => {
this.setState({ user: data });
});
}
render() {
return (
<div>
<p>User name:</p>
<p>{this.state.user.name}</p> // Renders without errors
</div>
);
}
}
从用户体验 (UX) 的角度来看,最好在获取数据之前显示某种加载器。
3. 忘记setState
异步
另一个常见的错误是尝试在设置状态值后立即访问它。
handleChange = count => {
this.setState({ count });
this.props.callback(this.state.count); // Old state value
};
设置新值不会立即生效,通常会在下一个可用渲染时完成,或者可以批量处理以优化性能。因此,在设置状态值后访问状态值可能无法反映最新更新。此问题可以通过使用 的可选第二个参数来解决setState
,该参数是一个回调函数,在状态更新为最新值后调用。
handleChange = count => {
this.setState({ count }, () => {
this.props.callback(this.state.count); // Updated state value
});
};
不过,使用 hooks 就完全不同了,因为 from 的 setter 函数useState
没有像 那样的第二个回调参数setState
。在这种情况下,官方推荐的方式是使用useEffect
hook。
const [count, setCount] = useState(0)
useEffect(() => {
callback(count); // Will be called when the value of count changes
}, [count, callback]);
const handleChange = value => {
setCount(value)
};
需要注意的是,它setState
并非异步的,因为它返回一个 Promise。因此,直接async/await
使用它或使用then
是行不通的(这也是一个常见错误)。
4. 错误地依赖当前状态值来计算下一个状态
该问题与上面讨论的问题相关,因为它也与状态更新异步有关。
handleChange = count => {
this.setState({ count: this.state.count + 1 }); // Relying on current value of the state to update it
};
这种方法的问题在于,在设置新状态时,count 的值可能无法正确更新,从而导致新状态值设置不正确。正确的方法是使用 的函数形式setState
。
increment = () => {
this.setState(state => ({ count: state.count + 1 })); // The latest state value is used
};
的函数形式setState
有第二个参数 - props
在应用更新时,可以以与状态类似的方式使用。
同样的逻辑适用于useState
钩子,其中 setter 接受一个函数作为参数。
const increment = () => {
setCount(currentCount => currentCount + 1)
};
5. 省略依赖数组 useEffect
这是一个不太常见的错误,但还是会发生。尽管省略 for 的依赖数组完全合理useEffect
,但当它的回调修改状态时,这样做可能会导致无限循环。
6. 将对象或其他非原始类型的值传递给 useEffect
的依赖数组
与上述情况类似,但更微妙的错误是跟踪 effect hook 依赖数组中的对象、数组或其他非原始值。请考虑以下代码。
const features = ["feature1", "feature2"];
useEffect(() => {
// Callback
}, [features]);
在这里,当我们将数组作为依赖项传递时,React 将仅存储对它的引用,并将其与数组的前一个引用进行比较。但是,由于它是在组件内部声明的,因此features
每次渲染时都会重新创建数组,这意味着它的引用每次都是一个新的,因此不等于 跟踪的引用useEffect
。最终,即使数组没有更改,回调函数也会在每次渲染时运行。对于原始值(例如字符串和数字)来说,这不是问题,因为在 JavaScript 中它们是按值而不是按引用进行比较的。
有几种方法可以解决这个问题。第一种方法是将变量声明移到组件外部,这样就不会在每次渲染时重新创建它。但是,在某些情况下这是不可能的,例如,如果我们跟踪 props 或被跟踪的依赖项是组件状态的一部分。另一种选择是使用自定义深度比较钩子来正确跟踪依赖项引用。一个更简单的解决方案是将值包装到useMemo
钩子中,这样在重新渲染期间就可以保留引用。
const features = useMemo(() => ["feature1", "feature2"], []);
useEffect(() => {
// Callback
}, [features]);
希望此列表能够帮助您避免最常见的 React 问题并提高对主要陷阱的理解。
对这篇文章有任何问题/评论或其他反馈吗?请在评论区或 Twitter上告诉我。
文章来源:https://dev.to/clarity89/the-most-common-mistakes-when-using-react-45h2