DEV Community

loading...

how I understood useCallback

nhidtran profile image nhidtran ・3 min read

useCallback was one of the new React hooks that I never really used in production until recently. I was a bit confused of its purpose or what was really happening behind the scenes.

sooo...
why? - you use it when you want to run this function after x,y,z changes. If you write a function that makes an api request, you might pass it another function to invoke when data arrives

async function someapicall(callback) {
  foobar()
  .then(somedatafromapi => 
  // invoke the callback function
  callback(somedatafromapi)
}
Enter fullscreen mode Exit fullscreen mode

Likewise, in React when x or y challenges, this callback function will return a new value of someObjectOrWhateverYouWant

const toggleSomething = useCallback(() => {
  // do something with the new x value
  // do something with the new y value
  // might invoke something here
  return someObjectOrWhateverYouWant
}, [x,y]
Enter fullscreen mode Exit fullscreen mode

An Experiment with hooks and useCallback

Recently I was playing around with this input.
Purpose: I was hoping to return the logic and a component from a hook (experiment! I don't think you should actually do that)
Problem: On each keystroke in the textarea. I would lose focus. Meaning, my component kept re-rendering

// useTextAreaInput
useTextAreaInput = () => {
  const [input, setInput] = React.useState("");

  const handleChange = React.useCallback(
    (val) => {
      setInput(val);
    },
    [setInput]
  );
  const RenderInput = ({ val }) => {
    // causes a re-render
    return (
      <textarea value={val} onChange={(e) => handleChange(e.target.value)} />
    );
  };

  return {
    input,
    RenderTextInput
  };
};

export default function App() {
  const { input: input2, RenderInput } = useTextAreaInput({
    initial: ""
  });
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <RenderTextInput val={input} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The fix:

To make this work, I would need to wrap the component I wanted to return in a callback. Why? Because when you wrap a function in useCallback, React "caches" it. Meaning, you get the same original reference or function in the lifetime that useTextArea is mounting. The reason it kept rendering, is that whenever there is a state change (whether in or within it's own state in useTextArea) it would return a new version of my "RenderInput" function. Likewise, React.useState never returns a new version of your useState function (setInput). That is also cached. So when you provide setInput (or handleChange also wrapped in a callback or "this", your RenderTextInput will never have to recompute and re-render

export const useTextAreaInput = () => {
  const [input, setInput] = React.useState("");

  const handleChange = React.useCallback(
    (val) => {
      setInput(val);
    },
    [setInput]
  );

  const RenderTextInput = React.useCallback(
    ({ val }) => {
      return (
        <textarea value={val} onChange={(e) => handleChange(e.target.value)} />
      );
    },
    [handleChange, setInput]
  );

  return {
    input,
    RenderInput,
    RenderTextInput
  };
};

export default function App() {
  const { input, RenderTextInput } = useTextAreaInput({
    initial: "test"
  });

  return (
    <div className="App">
      <h2>Start editing to see some magic happen!</h2>
      <RenderTextInput val={input} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

link to sandbox!

so that is a bit confusing and even when I realized what was happening, I confused myself. The best way for me to wrap my head around this, was to think about immutable and mutability in Javascript
This is a totally contrived way of thinking about it and disclaimer- I have no idea how React decides what to remember and what to toss out but...

TheTextAreaThatDoesNotRerender is going to be tied to this function. It is mutable, but it is always going to have the same reference whereas, TheTextAreaThatReRenders (the one not wrapped in a callback) is always going to return a new version of itself (an immutable version)

class useTextarea {
constructor(input) {
  this.value = input
}
TheTextAreaThatDoesNOTRerender(value) {
this.value = value
return this
}
TheTextAreaThatReRenders (val) {
  return new useTextArea(val)
}
Enter fullscreen mode Exit fullscreen mode

Something to keep in mind when writing out your components and playing around with hooks and knowing what side effects may occur when you have dependencies/state changes!

Discussion (0)

pic
Editor guide