4

What's the right way to create a memoized callback for use in components that have been created from a .map function?

ie. Here's a non-memoized version of what I want to achieve:

import * as React from "react";
import { render } from "react-dom";

const NUM_BUTTONS = 5;

function App() {
  const [value, setValue] = React.useState(0);

  return (
    <div className="App">
      <h1>Value is : {value}</h1>

      {new Array(NUM_BUTTONS).fill(true).map((v, i) => (
        <button key={i} onClick={() => setValue(i)}>
          {i}
        </button>
      ))}
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

(Code Sandbox)

Simple enough.

Now the problem is - I have that inline function which will cause those <button> components to re-render each time this component renders.

One approach I thought of, is to create a list of memoized callbacks, and map over them:

function App() {
  const [value, setValue] = React.useState(0);

  const callBacks = new Array(NUM_BUTTONS).fill(true).map((v, i) => {
    return React.useCallback(() => setValue(i), []);
  });

  return (
    <div className="App">
      <h1>Value is : {value}</h1>

      {callBacks.map((v, i) => (
        <button key={i} onClick={v}>
          i
        </button>
      ))}
    </div>
  );
}

(Code Sandbox).

But of course - this breaks one of the Rules of Hooks, you can't call a hook from inside a function or a conditional. Curiously enough, this code still works though.

The alternative I've got, is create a separate <SetValueButton> component that has the value bound to it, like so:


function SetValueButton(props) {
  const { value, onClick } = props;
  const handleClick = React.useCallback(() => onClick(value), [onClick, value]); 

  return <button onClick={handleClick}>{value}</button>;
}

function App() {
  const [value, setValue] = React.useState(0);

  const setValueCb = React.useCallback(i => setValue(i), []);
  return (
    <div className="App">
      <h1>Value is : {value}</h1>

      {new Array(NUM_BUTTONS).fill(true).map((v, i) => (
        <SetValueButton key = {i} onClick={setValueCb} value={i} />
      ))}
    </div>
  );
}

(Code Sandbox)

Is this the right idea, or is there a simpler way to do this?

3
  • but is it really neccessary? typically extra re-render goes literally in seconds if not cause DOM reflow. Using any approach(like passing data attribute, extracting interm component or any tricks with useRef) affects code readability/maintainability in bad way. Commented Sep 13, 2019 at 6:19
  • @skyboyer - This is just an example. What if for example, I had a grid of a thousand individually clickable grid items? Commented Sep 13, 2019 at 6:26
  • 1
    still see no issue with extra re-render(without reflow). motivation speech: kentcdodds.com/blog/… Commented Sep 13, 2019 at 7:43

1 Answer 1

4

The best way to avoid using an inline arrow function here without creating multiple functions is to make use of data-attribute

import * as React from "react";
import { render } from "react-dom";

const NUM_BUTTONS = 5;

function App() {
  const [value, setValue] = React.useState(0);
  const handleClick = React.useCallback((event) => {
      const id = event.target.getAttribute('data-index');
      setValue(id);
  }, [])
  return (
    <div className="App">
      <h1>Value is : {value}</h1>

      {new Array(NUM_BUTTONS).fill(true).map((v, i) => (
        <button key={i} data-index={i} onClick={handleClick}>
          {i}
        </button>
      ))}
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

Working demo

Sign up to request clarification or add additional context in comments.

5 Comments

@dwjohnston, does the above answer help you?
Do you really need to use React.useCallback here?
@Tom, No it isn't necessary to use useCallback here but it gives you an added optimisation in cases where you pass on the function to the child components which are either memoized or pure components
Hmm, I don't follow. Since the function is the same function passed down to child components, shouldn't memoized or pure components not re-render? I don't see why you need useCallback for that?
@Tom the <button> component would re-render every time App does because we are defining a new version handleClick each render. Using useCallback memoises the handleClick so that button receives identical props and can skip rerendering.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.