反应 useCallback 与参数

React useCallback with Parameter

使用 React 的 useCallback 钩子本质上只是 useMemo 的包装器,专门用于函数以避免在组件的 props 中不断创建新的函数实例。我的问题来自何时需要将争论传递给从备忘录创建的回调。

例如,像这样创建的回调...

const Button: React.FunctionComponent = props => {
    const onClick = React.useCallback(() => alert('Clicked!'), [])
    return <button onClick={onClick}>{props.children}</button>
}

是一个简单的记忆回调示例,不需要向其中传递任何外部值即可完成其工作。但是,如果我想为 React.Dipatch<React.SetStateAction> 函数类型创建一个通用的记忆回调,那么它将需要参数......例如:

const Button: React.FunctionComponent = props => {
    const [loading, setLoading] = React.useState(false)
    const genericSetLoadingCb = React.useCallback((x: boolean) => () => setLoading(x), [])

    return <button onClick={genericSetLoadingCb(!loading)}>{props.children}</button>
}

在我看来,这似乎与执行以下操作完全相同...

const Button: React.FunctionComponent = props => {
    const [loading, setLoading] = React.useState(false)
    return <button onClick={() => setLoading(!loading)}>{props.children}</button>
}

这会破坏记忆函数的目的,因为它仍然会在每个渲染器上创建一个新函数,因为 genericSetLoadingCb(false) 也会在每个渲染器上返回一个新函数。

这种理解是否正确,或者用参数描述的模式是否仍然保持记忆化的好处?

动机和问题陈述

让我们考虑以下(类似于您的 genericSetLoadingCb)高阶函数 genericCb:

  const genericCb = React.useCallback(
    (param) => (e) => setState({ ...state, [param]: e.target.value }),
    []
  );

假设我们在以下情况下使用它,其中 Input 是使用 React.memo 创建的 memoized 组件:

  <Input value={state.firstName} onChange={genericCb('firstName')} />

由于 Input 是记忆组件,我们可能希望 genericCb('firstName') 生成的 函数在重新渲染时保持不变,以便记忆组件不会不必要地重新渲染。下面我们将看到如何实现这一点。

解决方案

现在,我们在上面构造 genericCb 的方式是确保它在渲染中保持相同(由于使用 useCallback)。

但是,每次调用 genericCb 以创建一个新函数时,如下所示:

genericCb("firstName") 

returned 函数在每个渲染器上仍然会有所不同。 为了确保 returned 函数针对某些输入被记忆,您还应该使用一些记忆方法:

  import memoize from "fast-memoize";
  ....

  const genericCb = React.useCallback(
    memoize((param) => (e) => setState({ ...state, [param]: e.target.value })),
    []
  );

现在,如果您调用 genericCb("firstName") 生成一个函数,它将在每个渲染器上 return 相同的函数,前提是 "firstName" 也保持不变.

备注

正如上面评论中所指出的那样,使用 useCallback 的解决方案似乎会产生警告(虽然它没有出现在我的项目中):

React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead

似乎警告是因为我们没有将内联函数传递给 useCallback。我发现基于 this github 线程摆脱此警告的解决方案是使用 useMemo 模仿 useCallback 像这样:

// Use this; this doesn't produce the warning anymore  
const genericCb = React.useMemo(
    () =>
      memoize(
        (param) => (e) => setState({ ...state, [param]: e.target.value })
      ),
    []
  );

在没有 useCallback 的情况下简单地使用 memoize(或更新中的 useMemo)是行不通的,因为在下一次渲染时它会像这样从 fresh 调用 memoize:

let memoized = memoize(fn)
 
memoized('foo', 3, 'bar')
memoized('foo', 3, 'bar') // cache hit

memoized = memoize(fn); // without useCallback (or useMemo) this would happen on next render 

// Now the previous cache is lost

看来执行以下操作是解决您的问题的一种优雅而简单的方法。如果 Button 只是重新渲染,它不会创建新的 cb 函数。

const Button = props => {
    const [loading, setLoading] = React.useState(false)
    const cb = React.useCallback(() => { setLoading(!loading) }, [loading]);
    return <button onClick={cb}>{props.children}</button>
}