反应 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>
}
使用 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>
}