useEffect 介绍
官方解释:useEffect 是一个 React 钩子,可以让你 将组件与外部系统同步。
个人理解:useEffect 是一个重要的 hook ,可以实现函数组件中执行各种副作用操作,(比如:发送网络请求,手动变更 DOM,记录日志等)
语法
- setup 是一个执行函数。
- dependencies 是依赖项,是可选的。
当不传 dependencies 依赖项时,组件每次渲染都会导致 setup 函数的重新执行。当 dependencies 是一个空数组的时候,在组件渲染完成后会执行 setup 函数。当 dependencies 中存在依赖项时,组件第一次渲染会和依赖项发生改变都会执行 setup 函数。
useEffect(setup, dependencies?)
用法
1. useEffect 分别模拟了类组件中的哪些生命周期?
1.1 将 useEffect 的依赖项设置成一个空数组,这个时候 useEffect 相当于类组件中的 componentDidMount ,在组件挂载之后执行。
useEffect(() => {
// 相当于 componentDidMount 中的代码
console.log('组件挂载后执行')
}, [])
1.2 将 useEffect 的依赖项设置成监听的变量,这个时候 useEffect 相当于类组件中的 componentDidUpdate ,在组件的依赖值发生改变时执行。
const Simulation = () => {
const [count, setCount] = useState(0)
useEffect(() => {
// 相当于 componentDidUpdate 中的代码,当 count 变化时执行
console.log(`count 更新为: ${count}`)
}, [count])
return (
<div className="box">
<button onChange={() => setCount(a => a + 1)}>计数器:{count}</button>
</div>
)
}
1.3 将 useEffect 中返回一个清理函数,相当于类组件中的 componentWillUnmount,组件卸载前执行。
useEffect(() => {
// 组件挂载或更新时执行的代码
const timer = setInterval(() => {
console.log('每秒执行一次')
}, 1000)
// 返回一个清理函数,组件卸载前执行
return () => {
clearInterval(timer)
console.log('组件卸载,清除定时器')
}
}, [])
2.连接外部系统,控制模态对话框。
接下来这个案例是关于使用 useEffect 连接外部系统,控制模态对话框的操作,先看代码。
const ConnectionExternal = () => {
const ModalDialog = ({ isOpen, children }) => {
const ref = useRef()
useEffect(() => {
const dialog = ref.current
if (!isOpen) {
return
}
dialog.showModal()
return () => {
dialog.close()
}
}, [isOpen])
return <dialog ref={ref}>{children}</dialog>
}
const [show, setShow] = useState(false)
const openDialog = () => {
setShow(true)
}
const onClose = () => {
setShow(false)
}
return (
<div className="box">
<p>2.连接外部系统,控制模态对话框</p>
<button onClick={openDialog}>open dialog</button>
<ModalDialog isOpen={show}>
Hello!
<br />
<button onClick={onClose}>Close</button>
</ModalDialog>
</div>
)
}
代码演示
上面的案例中是通过父组件中一个 show 状态去控制子组件中模块框的显示和隐藏。ModalDialog 组件通过 useEffect 的副作用让 isOpen 属性控制到 showModal 方法和 close 方法的调用。
3. 根据副作用的先前状态更新状态。
一个常见的场景,在页面上设置一个计数的功能,每隔一秒钟让计数器 count 加 1,当然可以当 count 作为 useEffect 的依赖项,这也会导致每一次的 count 变化,useEffect 都会执行一次清理副作用和设置副作用的操作,例如:
const BeforeStateUpdate = () => {
const [count, setCount] = useState(0)
// 第一种使用count作为副作用的依赖值
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => {
console.log('intervalId', intervalId)
clearInterval(intervalId)
}
}, [count])
return (
<div className="box">
<div>状态:{count}</div>
</div>
)
}
对于上面的 useEffect 的频繁操作并不是必要的,可以利用 set 函数的中传递一个更新函数去解决。
const BeforeStateUpdate = () => {
const [count, setCount] = useState(0)
// 第二种不使用count作为副作用的依赖值
useEffect(() => {
const intervalId = setInterval(() => {
setCount(a => a + 1)
}, 1000)
return () => {
console.log('intervalId', intervalId)
clearInterval(intervalId)
}
}, [])
return (
<div className="box">
<div>状态:{count}</div>
</div>
)
}
4. 避免将对象或者函数作为 useEffect 的依赖项。
在 useEffect 的依赖项选择中,尽量避免使用对象或者函数作为依赖,当使用函数或者对象作为依赖项并在执行函数中去修改它,这样很有可能就会造成一种死循环,如下面这种。
const NeedlessObjectRely = () => {
const [person, setPerson] = useState({
name: 'zangsan',
sex: 'man'
})
useEffect(() => {
// 🙅错误示范: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
setPerson({
...person,
name: 'lisi'
})
}, [person])
return (
<div className="box">
<p> </p>
<div>姓名:{person.name}</div>
</div>
)
}
如果你必要在 useEffect 中去修改对象的属性,可以使用 set 函数时使用更新函数的形式。
const NeedlessObjectRely = () => {
const [person, setPerson] = useState({
name: 'zangsan',
sex: 'man'
})
// 🙆正确做法
useEffect(() => {
setPerson(p => {
return {
...p,
name: 'lisi'
}
})
}, [])
return (
<div className="box">
<p> </p>
<div>姓名:{person.name}</div>
</div>
)
}
注意事项
-
检查副作用中的状态更新:如果副作用中执行了会导致组件重新渲染的状态更新,并且这个更新又触发了副作用的重新执行,就可能形成无限循环。
-
异步执行:useEffect 中的代码是异步执行的,即在组件渲染到屏幕之后执行。如果需要同步执行副作用(如立即更改 DOM 以避免闪屏),可能需要考虑使用 useLayoutEffect。 了解更多关于 useLayoutEffect 内容。
-
避免依赖项重复变化:如果组件的多个 props 或 state 同时变化,并且这些变化都会触发相同的副作用,应该考虑如何合并这些变化以减少副作用的执行次数。