文章目录
本文主要介绍了使用react开发项目时比较容易出错或者是说需要注意的一些问题。
一、重复渲染问题
React
组件可能会在不同时间点重新渲染,如果不小心,某些操作可能会在每次渲染时都重复执行,这会降低性能。
解决办法: 可以使用useMemo
和useCallback
来记忆化操作,以便不会在每次渲染时重新计算。
示例: 用useMemo,避免在每次渲染时重复执行工作
function List({
items }) {
// 不使用useMemo - 每次渲染都会重新过滤
const filteredItems = items.filter(item => item.active);
// 使用useMemo - 只在items发生变化时重新过滤,
const filteredItems = useMemo(() => items.filter(item => item.active), [items]);
}
useMemo 和 useCallback
useMemo
是缓存函数本身,而useCallback
是缓存函数的返回值。
什么时候应该使用useMemo
而不是useCallback
?答案是:尽可能使用useMemo
,只有在必要时才使用useCallback
。
一个微妙但重要的区别:当函数本身被重复实例化并导致性能损耗时,使用useMemo
;否则,使用useCallback
是更好的选择。
当我们需要避免在渲染过程中创建函数本身时,useMemo
是合适的选择,而useCallback
无法阻止函数在每次出现时被重新创建。然而,useMemo
会确保如果依赖项没有发生变化,函数会返回缓存的值。
二、改变Hooks调用的顺序
React hooks
的内部工作原理要求组件在每次渲染的时候总是按相同的顺序执行hooks。
React依赖于Hooks调用的顺序来正确地关联Hook的状态。如果在循环或条件语句中调用Hooks,每次迭代或条件判断都可能产生新的Hook状态,这会导致React无法正确地关联状态和更新。
示例:
function fetchData() {
let [data, setData] = useState(null);
useEffect(() => {
fetch('api')
.then(response => response.json())
.then(data => setData(data));
}, []); // 依赖项为空,表示这个Effect只会在组件挂载时运行一次
// 错误写法:在循环中调用useState
for (let i = 0; i < 5; i++) {
useState(() => ({
count: 0 }));
}
}
React为Hooks定义的两条基本规则:
- 只能在函数组件的最顶层调用Hooks,不要在循环、条件或嵌套函数中调用Hooks。
- 只能在React的函数组件或自定义Hook中调用Hooks。
关于React的调度模型和Hooks的执行机制:
React的调度模型是基于Fiber架构的,它负责追踪和协调组件的更新,Hooks的执行也遵循这个模型。
当React渲染组件时,它会按照声明的顺序执行Hooks。如果在条件语句或嵌套函数中调用Hooks,React就无法保证这些Hooks的执行顺序,从而导致状态管理的混乱。
三、使用useState()不当
1、更新state对象属性时只改属性
使用setState()
更新对象属性时,非常容易犯的错误就是,只修改对象或数组的属性而不修改引用本身。
示例1:修改对象类型
import {
useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
name: "vickie",
age: 18,
});
// 出错处:更新用户状态属性,修改后user不再是一个对象,而是被改写为字符串"Cythia",而不是特定的属性被修改
// 原因:setState() 将返回或传递给它的任何值赋值为新状态
const changeName = () => {
setUser((user) => (user.name = "Cythia"));
};
// 正确做法
const changeName = () => {
// 方法1:创建一个新的对象引用,并将前一个用户对象分配给它
setUser((user) => Object.assign({
}, user, {
name: "Cythia" }));
// 方法2:推荐使用es6的扩展运算符(推荐)
setUser((user) => ({
...user, name: "Cythia" }));
};
return (
<div className='App'>
<p>User: {
user.name}</p>
<p>Age: {
user.age}</p>
<button onClick={
changeName}>Change name</button>
</div>
);
}
示例2: 修改数组类型
// 图片压缩组件
function Compress() {
const [files, setFiles] = useState([])
// 错误写法: 直接修改state的值
const handleChange = (newFiles) => {
api(newFiles).then((res)=>{
const cloneFiles = [...files] // 这里的files始终是[]
cloneFiles.map( /* 一些逻辑...*/ )
setFiles(cloneFiles)
})
}
// 正确写法:通过setState修改state的值
const handleChange = (newFiles) => {
api(newFiles).then((res)=>{
setFiles((oldFiles) => {
const cloneFiles = [...files] // 这里的files才是最新的
return cloneFiles.map( /*一些逻辑...*/ )
})
})
}
return <input type="upload" multiple onChange={
handleChange}/>
}
2、乱用useState()
1)在不需要render的场景下使用useState
// 不推荐,因为 return 部分并没有用到count状态,而每次setCount都会使组件重新渲染一次
function ClickButton(props){
const [count, setCount] = useState(0)
const onClickCount = () => {
setCount((c) => c + 1)
}
const onClickRequest = () => {
apiCall(count)
}
return (
<div>
<button onClick={
onClickCount}>Add</button>
<button onClick={
onClickRequest}>Submit</button>
</div>
)
}
// 正确做法
function ClickButton(props){
const count = useRef(0) // ref有个好处就是不会触发组件的重新渲染,从而避免了不必要的性能问题。
const onClickCount = () => {
count.current++ // ref本身是一个变化的对象,在组件渲染时使用ref.current来获取当前的值
}
const onClickRequest = () => {
apiCall(count.current)
}
return (
<div>
<button onClick={
onClickCount}>Click</button>
<button onClick={
onClickRequest}>Submit</button>
</div>
)
}
2)基础设施的数据,比如:渲染周期的细节(是否首次渲染,渲染次数)、计时器setTimeout()
, setInterval()
、对DOM
元素的直接引用等,应该使用引用useRef()
来保存和更新。
因为当useRef
的值发生变化时,不会导致React
引擎重新渲染,而useState
会触发重新渲染。
function MyComponent() {
const [count, setCount] = useState(0);
// const [isFirst, setIsFirst] = useState(true);
const isFirstRef = useRef(true); // 问题解决:将首次渲染的信息保存在引用isFirstRef中,isFirstRef.current属性用于访问和更新引用的值
useEffect(() => {
// 出现的问题:一旦更新了setIsFirst(false),就会触发重新渲染
// if (isFirst) {
// setIsFirst(false);
// return;
// }
if (isFirstRef.current) {
isFirstRef.current = false;
return;
}
}, [count]);
return (
<button onClick={
() => setCounter(count => count + 1)}>
Increase
</button>
);
}
四、使用 Ref 时出现的问题
1、忘记使用fowardRef
React 不允许将 ref
传递给函数组件,除非它被forwardRef
包装起来
import {
useRef } from'react';
// 错误写法
const CustomInput = ({
ref,...rest }) => {
const [value, setValue] = useState('');
useEffect(() => {
if(ref.current === null) return;
ref.current.focus();
}, [ref]);
return (
<input ref={
ref} {
...rest} value={
value} onChange={
e => setValue(e.target.value)} />
);
}
// 正确写法:用forwardRef包裹组件
const CustomInput = forwardRef((props, ref) => {
const [value, setValue] = useState('');
useEffect(() => {
if(ref.current === null) return;
ref.current.focus();
}, [ref]);
return (
<input ref={
ref} {
...props} value={
value} onChange={
e => setValue(e.target.value)} />
);
})
export const CustomInputElement = () => {
const ref = useRef();
return (
<CustomInput ref={
ref} />
);
}
2、不缓存 ref 的值
调用函数来初始化ref
的值时,需要缓存该函数或在渲染期间初始化 ref
(在检查值尚未设置之后)
示例:
import {
useState, useRef, useEffect } from "react";
const useOnBeforeUnload = (callback) => {
useEffect(() => {
window.addEventListener("beforeunload", callback);
return () => window.removeEventListener("beforeunload", callback);
}, [callback]);
}
export const App = () => {
// 错误写法:函数开销很大时,将不必要地影响应用性能
// const ref = useRef(window.localStorage.getItem("cache-date"));
// 正确写法
const ref = useRef(null);
if (ref.current === null) {
ref.current = window.localStorage.getItem("cache-date");
}
const [inputValue, setInputValue] = useState("");
useOnBeforeUnload(() => {
const date = new Date().toUTCString();
console.log("Date", date);
window.localStorage.setItem("cache-date", date);
});
return (
<>
<div>缓存的时间: <strong>{
ref.current}</strong></div>
用户名:{
" "}
<input value={
inputValue} onChange={
(e) => setInputValue(e.target.value)} />
</>
);
}
五、不正确的使用useEffect()
class
组件时,componentDidMount
是一个通用的生命周期函数,用来做一些数据请求,事件绑定等。在使用Hooks
时,useEffect
则取代了生命周期函数。但是不正确的使用 useEffect
可能会导致一些问题。
示例:不正确的使用 useEffect
可能会导致最终创建多个事件绑定
// 不良写法 - 每次渲染都会创建新的事件监听器
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
});
// 推荐写法 - 只在组件挂载时创建事件监听器
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize); // 不要忘记清理副作用,如果用到的话
};
}, []); // 空的依赖数组,通过使用空的依赖数组,确保了事件监听器只在组件挂载时创建一次
useEffect
钩子用于处理副作用,但如果不正确使用它,可能会导致创建多个事件监听器,这会引发问题。在上述示例中,可以看到正确使用useEffect
的方法包括将清理函数返回以取消订阅,以及使用空的依赖数组以确保只运行一次。
示例2:
function FetGame({
id }) {
const [game, setGame] = useState({
name: '',
description: ''
});
useEffect(() => {
const fetchGame = async () => {
const response = await fetch(`/api/game/${
id}`);
const fetchedGame = await response.json();
setGame(fetchedGame);
};
if (id) {
fetchGame();
}
}, [id]);
// 避免条件渲染hook的实用性建议:在组件主体的顶部执行hooks,逻辑渲染语句移到组件底部。
if (!id) {
return 'Please select a game to fetch';
}
return (
<div>
<div>Name: {
game.name}</div>
<div>Description: {
game.description}</div>
</div>
);
}
useEffect用法
useEffect()
可以看做是类组件中componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个生命周期函数的组合。
useEffect(effect, deps)
接收 2 个参数:
effect
副作用函数;deps
依赖项数组。
useEffect
使用的 4 种情况:
// 1、第二个参数不传:任何状态更新,都会触发useEffect的副作用函数
useEffect(() => {
setCount(count + 1);
});
// 2、第二个参数为空数组:仅在挂载和卸载的时触发useEffect的副作用函数
useEffect(() => {
setCount(count + 1);
}, []);
// 3、第二个参数为单值数组:仅在该值变化,才会触发useEffect的副作用函数
useEffect(() => {
setCount(count + 1);
}, [name]);
// 4、第二个参数为多值数组:仅在传入的值发生变化,才会触发useEffect的副作用函数
useEffect(() => {
setCount(count + 1);
}, [name, age]);
// 当需要实现componentWillUnmount 生命周期函数的效果,可以在useEffect()函数返回一个函数即可,该函数会在组件卸载时执行
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count+1);
return () => {
console.log('[组件已卸载]');
}
}, []);
return <div className="App">{
count}</div>;
}
六、props 透传
props
透传是指将单个 props
从父组件向下多层传递的做法。理想状态下,props
不应该超过两层,当我们选择多层传递时,会导致一些性能问题。
props 透传会导致不必要的重新渲染:因为React 组件总会在 props
发生变化时重新渲染,而那些不需要 props,只是提供传递作用的中间层组件都会被渲染。
解决办法: 方法有很多,比如 React Context Hook
,或者类似 Redux
的库,简单的项目选择使用 Context Hook
是更好的选择,使用 Redux
需要额外编写一些代码,更适合单个状态改变很多东西的复杂场景。
示例1:用React的上下文context来共享数据
import React, {
Component } from 'react';
import {
createContext, useContext, useState } from 'react';
// 创建了一个名为MyContext的上下文
const MyContext = createContext();
// 创建了一个名为MyProvider的组件来提供共享的数据
const MyProvider = ({
children }) => {
const [data, setData] = useState('Hello, World!');
return (
<MyContext.Provider value={
{
data, setData }}>
{
children}
</MyContext.Provider>
);
};
const MyConsumer = () => {
// 用useContext钩子来获取上下文中的数据,并使用setData函数来更新数据
const {
data, setData } = useContext(MyContext);
return (
<div>
<h1>{
data}</h1>
<button onClick={
() => setData('Hello, Button!')}>Change Data</button>
</div>
);
};
// 在App组件中包装了MyProvider和MyConsumer组件。
const App = () => {
return (
<MyProvider>
<MyConsumer />
</MyProvider>
);
};
context共享数据总结:
- 16.3 之前的老版本
- 提供者:
getChildContext + static childContextType
- 消费者:
this.context.xx 取值
- 提供者:
- 16.3 之后的版本:
- 类组件:
React.createContext
- 提供者:
TopContext.Provider
- 消费者:
TopContext.Consumer
- 优化方式一:
组件.contextType= TopContext; this.context.xx取值
- 优化方式二:封装
connect
函数,直接从组件参数获取
- 优化方式一:
- 提供者:
- 函数组件(hook):
- 提供者:
createContext.Provider
- 消费者:
useContext(TopContext)
返回值
- 提供者:
- 类组件:
示例2:用hooks实现数据传递
// 父组件:创建+包裹
export const ContactContext = createContext({
});
function parent(){
<ContactContext.Provider value={
{
itemData: res }} >
<ShowName />
<ShowSex/>
</ContactContext.Provider>
}
// 子组件接收数据:useContext(父定义的context)
const ShowName = () => {
const {
itemData:{
name}} = useContext(ContactContext);
return ( <span className='name'>{
name}</span> )
}
React的数据传递方式:
- props:只能父传子
- redux:数据传递不多的时候使用会有点重
- 数据共享:context 上下文