43

With react hooks now available should I in case of functional components wrap every function passed with props with useCallback and every other props value with useMemo?

Also having custom function inside my component dependent on any props value should I wrap it with useCallback?

What are good practices to decide which props or const values from component wrap with this hooks ?

If this improves performance why not to do it at all times ?

Lets consider custom button where we wrap click handler and add custom logic

function ExampleCustomButton({ onClick }) {
  const handleClick = useCallback(
    (event) => {
      if (typeof onClick === 'function') {
        onClick(event);
      }

      // do custom stuff

    },
    [onClick]
  );

  return <Button onClick={handleClick} />;
}

Lets consider custom button where we wrap click handler and add custom logic on condition

function ExampleCustomButton({ someBool }) {
  const handleClick = useCallback(
    (event) => {
      if (someBool) {
        // do custom stuff
      }
    },
    [someBool]
  );

  return <Button onClick={handleClick} />;
}

Should i in this two cases wrap my handler with useCallback ?

Similar case with use memo.

function ExampleCustomButton({ someBool }) {
  const memoizedSomeBool = useMemo(() => someBool, [someBool])
  const handleClick = useCallback(
    (event) => {
      if (memoizedSomeBool) {
        // do custom stuff
      }
    },
    [memoizedSomeBool]
  );

  return <Button onClick={handleClick} />;
}

In this example I even pass memoized value to useCallback.

Another case what if in the component tree many components memoize same value ? How does this impact performance ?

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
vardius
  • 6,326
  • 8
  • 52
  • 97
  • Well, definitely not every time, all the time : ) Their use is situational. I'd say look into actually using these when you start experiencing performance hurdles. – Pa Ye Mar 25 '19 at 12:58
  • 1
    Thanks @Powell_v2 for your comment, but if this improves performance why not to do it at all times ? – vardius Mar 26 '19 at 05:29
  • I guess if that was viable, React would to it automatically instead of exposing these as separate APIs. As @jalooc pointed out, there are costs associated with memoizing callbacks/values. Code cluttering is also a concern. – Pa Ye Mar 27 '19 at 13:00
  • If the click handler **doesn't use** any of the component's `props` within it, then you can declare it outside of the component and then the handler function won't get re-created on every prop change of the component (since it's outside..) – vsync Apr 01 '19 at 08:20
  • Should also add that placing useMemo inside of useCallback is invalid. useMemo can only be called inside React function components or custom React hooks. – DSLuminary Feb 19 '20 at 01:19
  • useCallback with React.memo however is valid, in case readers or the author want to see the difference between combining the two and not maybe this answer can provide some insight https://stackoverflow.com/a/60586324/5663961 – Gunnar Már Óttarsson Mar 08 '20 at 10:39

3 Answers3

48

Not worth it, for multiple reasons:

  1. Even official docs say you should do it only when necessary.
  2. Keep in mind that premature optimization is the root of all evil :)
  3. It makes DX (developer experience) far worse: it's harder to read; harder to write; harder to refactor.
  4. When dealing with primitives (like in your example) memoizing costs more CPU power than not doing it. A primitive value doesn't have a concept of references, so there's nothing to memoize in them. On the other hand, memoization itself (as any other hook) does require some tiny processing, nothing is for free. Even though it's tiny, it's still more than nothing (compared to just passing a primitive through), so you'd shoot your own foot with this approach.

To put all together - you'd waste more time typing all the hooks than a user would gain on having them in the application if you want to put them everywhere. The good old rule applies: Measure, then optimize.

jalooc
  • 1,169
  • 13
  • 23
  • 15
    "Do it only when necessary" doesn't answer anything. When is it "necessary"? Changing the value of the prop to the re-creaded function will re-render the child component(s). So I guess it depends on the cost of re-rendering the affected child components vs the cost of using `useCallback()`. – Qtax Sep 05 '19 at 17:28
  • 1
    "when necessary" is described in the following paragraphs, which say you should wrap it after you measure and find out that the particular spot is a source of slowness. – jalooc Sep 06 '19 at 18:11
  • 1
    Part of a quote, with a smiley at that. At least complete it: _Yet we should not pass up our opportunities in that critical 3%._ [An excellent article on this.](https://okaleniuk.medium.com/premature-optimization-is-the-root-of-all-evil-is-the-root-of-evil-a8ab8056c6b) – Wolfzoon Dec 03 '21 at 12:06
  • 4) Memoizing something does involve a tiny processing power, but that is immediately gained on subsequent renders – Qwerty May 17 '22 at 14:36
  • 1
    1) When the recipient of the prop is a DOM element - don't waste your resources. If it's a *leaf* Component and the *parent* renders very very often, then it might be worth wrapping the parent->child props in `useCallback()`, especially when they don't change much, such as event handlers. If the parent itself does not render often, then don't waste your time. If you are deep in the component tree and the callbacks are not supposed to change much, then it's absolutely beneficial for you to use `useCallback()`. – Qwerty May 17 '22 at 14:41
22

I agree with the principles proposed by @jalooc

To give some more insight about the exhibited use cases in the OP, here is my advice:

Expensive children renders

function Component() {
  const callback = useCallback(() => { dostuff }, [deps])

  return <Child prop={callback} />
}

The above makes sense if Child is a very expensive component to render. As such, it is probably exported like so:

function Child() { 
   ...this takes significant CPU... 
}

// Export as a pure component
export default React.memo(Child)

Expensive computations

function Component({ foo }) {
  // This very expensive computation will only run when it's input (foo)
  // changes, allowing Component to re-render without performance issues
  const bar = useMemo(() => {
     ... something very complicated with `foo` ...
  }, [foo])

  return <div>{bar}</div>
}

Conclusion

  1. Do what makes sense or that have measured bad performance
  2. A function declaration inside a component changes at each render. If this causes derived expensive computations, memoize it (useCallback) or move it outside the scope.
  3. If a component itself is expensive to render, make it pure with React.memo, with the help of #2 if necessary
  4. If something IS itself expensive to re-compute, memoize it (useMemo)
  5. Do what makes sense or that have measured bad performance
Pandaiolo
  • 11,165
  • 5
  • 38
  • 70
  • 2
    Also, `useMemo` is necessary when passing an object, created during the render phase, into a component, wrapped with `React.memo` – Pavlo Zhukov Oct 27 '20 at 22:02
  • 1
    That is right for all objects created in parent component, passed to memoized component, but also all callbacks/function, that should be memoized - with the help of `useCallback` in that case. Baseline, if `memo`-wrapped component's props change too often, then `memo` will not be very useful. Thanks for the reminder. – Pandaiolo Oct 28 '20 at 23:06
  • 1
    I think the problem really is, how expensive does a function need to be in order to justify using a useMemo. "something very complicated" and "this takes significant CPU" are subjective. – rymanso Feb 15 '22 at 13:56
4

I'd argue that there's nothing wrong with putting useCallback everywhere you create a function. The same points are applicable to useMemo; but I'll be mentioning only useCallback since, for the sake of brevity.

As a concession, I must mention that this isn't a cornerstone in your development process, and you must proceed with a solution that your team is comfortable with, whether it's using useCallback or not.

My main argument for using such memoization hooks "extensively" is that you don't have to think of potential performance issues on the front of "reference update-induced" rerendering if you do it. Fewer things to think about = good. More time and energy to solve the actual problems.

Many say "premature optimization is bad". Well, cargo cult isn't good either, and this citation is a pure example of such, taken too far out of context, and with a premise of having an authority behind it. here used to be a good summary on that, unfortunately, the account is taken down currently, but it's available on archive.org.

The concerns of "premature optimization" in this case are structural changes to the code and impaired readability/write-ability.

Structurally, these changes are for the better, making you separate your components.

Readability-wise, there's an extra wrapper, but that comes together with strict tracking of your function dependencies, which otherwise is done implicitly. Dependencies become explicit as they should be since in this system they play a too important role to swipe it under the rug. Readability, therefore, only wins. Although it's harder to write now, the "ritual" is always the same. Readability is more important than the convenience of writing a function. (Just make sure you use eslint to track your dependencies; it could be a headache when you forget to add a dependency and it gets cached.)

So, "write now, optimize later" - thanks, I'd pass. To me, it's a negligibly cheap optimization that is justified enough and is reasonable enough, if your team is ready to accept the arguments above. It's also fine if they aren't: the topic itself isn't something to die for. It's just a small quality-of-life tool.

Igor Loskutov
  • 2,157
  • 2
  • 20
  • 33
  • 2
    Absolutely. It's annoying how many cargo cult devs use the "premature optimization = bad" argument and refuse to optimize unless presented performance benchmarks (which are very expensive in terms of dev time) to justify memoization. IMO, a good programmer shouldn't wait for a program to slow down before improving their code; they should be able recognize opportunities when evaluating expensive procedures (e.g. JSON.parse, accessing localstorage, etc.) and use their best judgement. – dlq Jul 29 '22 at 16:12
  • this sounds like a modern version of why "we should do everything on assembly so it works faster". wrapping an onClick callback applied on a button neither will end the climate change or will make a difference anyone could notice anyway. At that point I would argue optimizing (ie. deleting) thousands lines of useCallback wrappers to save disk space :) – Ali Mert Çakar Dec 13 '22 at 20:25
  • Please note that it's not about resources but rather about predictable behaviour which is, in turn, is about developer time spent on debugging. – Igor Loskutov Dec 14 '22 at 05:57