目录
3.如果一个值可以基于现有的 props 或 state 计算得出,不要把他作为state,而是在渲染期间直接计算这个值
基本解释
useEffect是 React 中的一个 Hook,用于在函数式组件中执行副作用操作。
副作用是指那些与组件渲染无关的操作,比如数据获取、订阅、手动修改 DOM 等。
基本使用
useEffect接受两个参数,第一个参数是根据依赖所执行的回调函数,第二个参数是依赖数组,如果是空数组,则代表无依赖,只在组件渲染后执行一次
生命周期
Mounting(挂载阶段)
- 当组件首次被渲染到 DOM 中时,useEffect 会在组件渲染后执行。
Updating(更新阶段)
- 在组件更新时如果不传入第二个参数,useEffect 会在每次渲染后执行。
- 如果 useEffect 中指定了依赖项数组(第二个参数),当依赖项发生变化时会先执行return 函数做清理工作,然后执行当前依赖变化后的副作用函数。
Unmounting(卸载阶段)
- 当组件从 DOM 中被卸载时,useEffect 会在组件卸载前执行。可以在这里进行一些清理工作,比如取消订阅、清除定时器等。
经典例子
1.避免依赖重复变化触发多个网络请求,导致数据更新错误。
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
在这个例子中,假设userId不存在为undefined的情况,组件渲染后根据依赖userId执行副作用函数获取todos,设置ignore是为了避免userId快速变化执行多个网络请求,导致多个网络请求处于竞争状态,多个setTodos延迟执行,就会导致UI视图和userId不对号。因为网络请求开始执行是无法取消的,所以设置ignore在依赖变化后重设ignore为true,就会解决这个问题。
2.副作用函数因为闭包的原因,需要添加依赖。
const [count, setCount] = useState(0)
useEffect(() => {
setInterval(() => {
setCount(count + 1) //setCount(0 + 1)
console.log(count) //console.log(0)
}, 1000)
}, [])
在这个例子中,组件加载完执行副作用函数,执行定时器代码每隔1秒让count + 1,但是因为闭包的原因count是被“定格的”,永远隔1秒都是打印的都是0,所以需要添加count依赖。
const [count, setCount] = useState(0)
useEffect(() => {
setInterval(() => {
setCount(count + 1)
console.log(count)
}, 1000)
}, [count])
但是这样之后 count并不是每1秒打印一次,而是打印多次,这是因为上一次的定时任务在依赖变化之后并没有清理掉还在继续执行,所以还需要添加清理任务
const [count, setCount] = useState(0)
useEffect(() => {
const timerId = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => {
clearInterval(timerId)
}
}, [count])
这样就能保证每1000ms打印一次递增后的count,但是这样写太冗余了,有一种更好的写法
const [count, setCount] = useState(0)
useEffect(() => {
const timerId = setInterval(() => {
setCount(c => c+ 1)
}, 1000)
return () => {
clearInterval(timerId)
}
}, [])
在setCount中用传入一个回调获取上一次的值再递增就可以了
3.如果一个值可以基于现有的 props 或 state 计算得出,不要把他作为state,而是在渲染期间直接计算这个值
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
在这个例子中,useEffect根据依赖firstName和lastName的变化去执行副作用函数计算出fullName,其实大可不必,不是说这样写不对,但我们应该本着能在渲染期间计算出来结果,就不要在渲染后计算结果再渲染,应该像下面这样写才更好。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName;
// ...
}
组件因为state变化重新渲染进而fullName也会重新计算,但是这个例子只是一个简单的字符串计算,并不会影响性能,但是如果涉及到复杂的消耗性能的计算,比如下面例子。
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 如果 getFilteredTodos() 的耗时不长,这样写就可以了。
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
这是一个TodoList组件,根据porps(todos和filter)来计算出visibleTodos数组,todos和filter是查询条件,这里是字符串,如果执行getFilteredTodos的耗时不长,上面写法就可以,但是假设计算非常耗时,则要考虑用useMemo来缓存计算结果,避免组件每次渲染重复大量的计算。
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// 除非 todos 或 filter 发生变化,否则不会重新执行
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
这会告诉 React,除非 todos 或 filter 发生变化,否则不要重新执行传入的函数。React 会在初次渲染的时候记住 getFilteredTodos() 的返回值。在下一次渲染中,它会检查 todos 或 filter 是否发生了变化。如果它们跟上次渲染时一样,useMemo 会直接返回它最后保存的结果。如果不一样,React 将再次调用传入的函数(并保存它的结果)。
4.避免将函数或对象作为依赖
function ChatRoom({ roomId, serverUrl }) {
const [message, setMessage] = useState('');
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]);
return (
<>
<h1>欢迎来到 {roomId} 房间!</h1>
<input value={message} onChange={e => setMessage(e.target.value)} />
</>
);
}
这是一个Chatroom组件,props接收一个roomId,createConnection是连接chat的一个接口,写法乍一看没什么问题,roomId和serverUrl的变化会导致options重新计算,进而useEffect会根据options的变化执行副作用函数,关闭连接,再重新创建连接,也没什么问题,但是忽略了一个问题,这是对象作为依赖,是引用传值,ChatRoom的props变化以及state变化,甚至父组件state的变化,都是导致组件重新渲染,对象重新创建,自然会重新执行副作用函数,进而导致大量不必要的断开连接和重新连接,所以避免对象传值,应该下面这样写。
function ChatRoom({ roomId, serverUrl }) {
const [message, setMessage] = useState('');
// function createOptions() { //写法1
// return {
// serverUrl: serverUrl,
// roomId: roomId
// };
// }
//const options = createOptions()
const options = { //写法2
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]);
return (
<>
<h1>欢迎来到 {roomId} 房间!</h1>
<input value={message} onChange={e => setMessage(e.target.value)} />
</>
);
}
5.避免传入多个互不相关的依赖
//写法不友好
useEffect(() => {
//to do something about vara
//to do something about varb
//to do something about varc
}, [vara, varb, varc]);
//写法友好
useEffect(() => {
//to do something about vara
}, [vara]);
useEffect(() => {
//to do something about varb
}, [varb]);
useEffect(() => {
//to do something about varc
}, [varc]);