useEffect
Effect Hook 可以让你在函数组件中执行带副作用的操作。
什么是 Effects?
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都是常见的(副)作用。
在 React 组件中有两种常见的副作用操作,一些在完成后需要清理,另一些不需要清理。我们将在稍后的例子中研究它们的区别,但首先让我们看看 useEffect 接口。
基本用法
// 每次渲染完成后运行
React.useEffect(() => {
// Run effects
None // or Some(() => {})
})
// 组件 mount 后运行一次
React.useEffect0(() => {
// Run effects
None // or Some(() => {})
})
// `prop1` 变更时运行
React.useEffect1(() => {
// Run effects based on prop1
None
}, [prop1])
// `prop1` or `prop2` 变更时运行
React.useEffect2(() => {
// Run effects based on prop1 / prop2
None
}, (prop1, prop2))
React.useEffect3(() => {
None
}, (prop1, prop2, prop3));
// useEffect4...7 with according dependency
// tuple just like useEffect3
React.useEffect
接收一个 effect 函数,包含命令式的,可能有副作用的代码,然后返回一个值 option<unit => unit>
作为潜在的清理函数。
useEffect
调用可能会接收额外的依赖项数组(参见 React.useEffect1
/ React.useEffect2...7
)。只要依赖项之一发生变化,就会运行 effect 函数。关于这些接口的用处请看下面。
注意: 你可能好奇为什么 React.useEffect1
接收依赖项的 array
,但 useEffect2
需要一个依赖项的 tuple
(例如 (prop1, prop2)
)。因为元组可以存储不同类型的多个值,数组只能存储某个特定类型的值。可以用 React.useEffect1(fn, [1, 2])
模仿 useEffect2
,但是类型检查器不会允许 React.useEffect1(fn, [1, "two"])
通过。
React.useEffect
会在每次渲染完成时运行它的 effect 函数,React.useEffect0
只在首次渲染时(组件 mounted 时)运行 effect 函数。
示例
不需要清理的 Effects
有时,我们想要在 React 更新完 DOM 之后运行一些额外的代码。常见的不需要清理的 effects 有网络请求,手动操作 DOM 以及记录日志。因为运行它们之后就可以不管了。
下面是一个计数器组件的例子,在每次渲染时更新 document.title
:
// Counter.res
module Document = {
type t;
@val external document: t = "document";
@set external setTitle: (t, string) => unit = "title"
}
@react.component
let make = () => {
let (count, setCount) = React.useState(_ => 0);
React.useEffect(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None
}, );
let onClick = (_evt) => {
setCount(prev => prev + 1)
};
let msg = "You clicked" ++ Belt.Int.toString(count) ++ "times"
<div>
<p>{React.string(msg)}</p>
<button onClick> {React.string("Click me")} </button>
</div>
}
我们想要 effects 依赖于 count
,可以使用下面的 useEffect
调用:
RES React.useEffect1(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None
}, [count]);
现在它只在 count
的值改变时运行 effect 函数,而不是在每次渲染时都运行。
需要清理的 Effects
上面我们研究了如何表达不需要任何清理的副作用。然而某些 effects 需要进行清理,例如订阅一些外部数据源。在这种情况下,清理很重要,否则可能会产生内存泄露!
让我们看一个例子,它可以优雅地订阅和取消订阅一些订阅 API:
// FriendStatus.res
module ChatAPI = {
// Imaginary globally available ChatAPI for demo purposes
type status = { isOnline: bool };
@val external subscribeToFriendStatus: (string, status => unit) => unit = "subscribeToFriendStatus";
@val external unsubscribeFromFriendStatus: (string, status => unit) => unit = "unsubscribeFromFriendStatus";
}
type state = Offline | Loading | Online;
@react.component
let make = (~friendId: string) => {
let (state, setState) = React.useState(_ => Offline)
React.useEffect(() => {
let handleStatusChange = (status) => {
setState(_ => {
status.ChatAPI.isOnline ? Online : Offline
})
}
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
setState(_ => Loading);
let cleanup = () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
}
Some(cleanup)
})
let text = switch(state) {
| Offline => friendId ++ " is offline"
| Online => friendId ++ " is online"
| Loading => "loading..."
}
<div>
{React.string(text)}
</div>
}
Effect 依赖
在某些情况下,在每次渲染时进行清理或运行 effect 函数会产生性能问题。让我们看一个具体的例子,看看 useEffect
会怎么做:
RES// from a previous example above
React.useEffect1(() => {
open Document
document->setTitle(`You clicked ${Belt.Int.toString(count)} times!`)
None;
}, [count]);
这里,我们将 [count]
作为依赖传给 useEffect1
。这是什么意思?如果 count
的值是 5,我们的组件重新渲染时 count
的值依然是 5,React 会比较前次渲染的 [5]
和之后渲染的 [5]
。因为数组中所有项是相同的(5 === 5),React 会跳过 effect 函数。这就优化了性能。
当我们使用更新为 6 的 count
进行渲染时,React 会比较前次渲染的 [5]
和之后渲染的 [6]
。这次,React 将重新运行 effect 函数,因为 5 !== 6
。如果数组中有多个项,即使其中只有一个不同,React 也会重新运行 effect 函数。
这也适用于有清理阶段的 effects:
RES// from a previous example above
React.useEffect1(() => {
let handleStatusChange = (status) => {
setState(_ => {
status.ChatAPI.isOnline ? Online : Offline
})
}
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
setState(_ => Loading);
let cleanup = () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
}
Some(cleanup)
}, [friendId]) // Only re-subscribe if friendId changes
重要: 如果使用此优化,请确保数组包含组件范围中随时间变化且被 effect 使用的所有值(例如 props 和 state)。否则,代码将引用前一次渲染的过时值。
如果要只在 mount 时运行一次 effect,然后在 unmount 时进行清理,使用 React.useEffect0
。
如果你对有关性能优化的主题感兴趣,请参考 ReactJS 性能优化文档。