React useCallback with Parameter
Asked Answered
P

3

58

Using React's useCallback hook is essentially just a wrapper around useMemo specialized for functions to avoid constantly creating new function instances within components' props. My question comes from when you need to pass an argued to the callback created from the memoization.

For instance, a callback created like so...

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

is a simple example of a memoized callback and required no external values passed into it in order to accomplish its job. However, if I want to create a generic memoized callback for a React.Dipatch<React.SetStateAction> function type, then it would require arguments...for example:

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>
}

In my head, this seems like its the exact same as doing the following...

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

which would let defeat the purpose of memoizing the function because it would still be creating a new function on every render since genericSetLoadingCb(false) would just be returning a new function on each render as well.

Is this understanding correct, or does the pattern described with arguments still maintain the benefits of memoization?

Phalanstery answered 16/4, 2020 at 16:20 Comment(0)
M
59

I will provide an answer with a slightly different use case, but it will still answer your question.

Motivation and Problem Statement

Let's consider following (similar to your genericSetLoadingCb) higher order function genericCb:

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

Say we use it in the following situation where Input is a memoized component created using React.memo:

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

Since Input is memoized component, we would like the function generated by genericCb('firstName') to remain the same across re-renders, so that the memoized component doesn't re-render needlessly.

Below we will see how to achieve this.

Solution

Now, the way we constructed genericCb above is we ensured that it remains the same across renders (due to usage of useCallback).

However, each time you call genericCb to create a new function out of it like this:

genericCb("firstName") 

The returned function will still be different on each render. To also ensure the returned function is memoized for some input, you should additionally use some memoizing approach:

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

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

Now if you call genericCb("firstName") to generate a function, it will return same function on each render, provided "firstName" also remains the same.

Remarks

As pointed out in the comments above solution using useCallback seems to produce warning (it didn't in my project though):

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

It seems the warning is there because we didn't pass inline function to useCallback. The solution I found to get rid of this warning based on this github thread is to use useMemo to imitate useCallback like this:

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

Also I would like to note that simply using memoize without useCallback (or useMemo as in the update) wouldn't work, as on next render it would invoke memoize from fresh like this:

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
Merengue answered 16/4, 2020 at 17:55 Comment(10)
ok, so my assumption that using just React.useCallback on a argument receiving HOF provides no value unless you also memoize the internal function within the useCallback usage.Phalanstery
@Phalanstery I believe so as useCallback would memoize the function you passed to it, not the function which is created when you invoke genericSetLoadingCbMerengue
would it be better the use React.useMemo or an inner React.useCallback instead of fast-memoize to reduce dependencies?Phalanstery
@Phalanstery memoize will create different function for memoizedcb(true) and for memoizedcb(false). Can you make useCallback dependant on argument like that?Merengue
In TypeScript, it gives us this eslint warning: "React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.". What to put in dependency array instead of disabling the eslint warning?Strawberry
@JeafGilbert I am afraid I don't know what is best way to go in that case, I didn't get that warning, I don't use Typescript. Maybe you can ignore it, if you make sure you don't have stale closures.Merengue
the eslint warning shows up on js project, tooSyllabary
@GiorgiMoniava thanks so much, that was very genius solution, with useMemo and momize, that worked for me, but fast-memoize package did not work for me. I used Lodash momoizeGastric
@Gastric I don't know why fast-memoize didn't work for you, for me it did.Merengue
@GiorgiMoniava, the only thing that is different from your example is that my first parameter is not a string type, but a function type.Gastric
M
2

I suggest creating a map of memoized callbacks for each possible argument value:

const genericSetLoadingCb = React.useMemo(() =>
  {
    [true]: () => setLoading(true),
    [false]: () => setLoading(false),
  },
  []
);

Thus a memoized version will be the same for each value:

return <button onClick={genericSetLoadingCb[!loading]}>{props.children}</button>
Macbeth answered 28/1, 2023 at 7:12 Comment(0)
C
0

Try to use callback for inner function:

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

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

So every time you call genericSetLoadingCb it would return memoized function. It works but react recommends to place useCallback inside component directly or inside another hook.

Choirmaster answered 20/3 at 13:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.