文档 / rescript-react / useEffect Hook
Edit

useEffect

Effect Hook 可以让你在函数组件中执行带副作用的操作。

什么是 Effects?

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都是常见的(副)作用。

在 React 组件中有两种常见的副作用操作,一些在完成后需要清理,另一些不需要清理。我们将在稍后的例子中研究它们的区别,但首先让我们看看 useEffect 接口。

基本用法

ReScriptJS Output
// 每次渲染完成后运行
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

ReScriptJS Output
// 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:

ReScriptJS Output
// 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 性能优化文档