If you want some context for this series, please see the following posts about functional React components and the hooks API:

  1. React Functional Components
  2. The useState Hook
  3. The useEffect Hook

In this post I'm going to examine the useCallback hook. To understand what the useCallback hook does and why it's necessary for React functional components, it's important that we talk briefly about when React components re-render. React is really good about re-rendering only necessary components; it does this by doing equality checking on a component's props. If you're really new to React, "props" are the React equivalent of HTML attributes - key/value pairs on a dom node - that get passed into a component. For example, if we render a component thusly:

<Banana color="yellow" ripe={true} />  

Then the Banana component would receive a props object that looks like this:

{
  color: 'yellow',
  ripe: true
}

which it could accept like this:

const Banana = ({ color, ripe }) => {  
  return <div>My {color} banana is {ripe ? 'ripe' : 'unripe'}</div>
};

Whenever the reference equality of one of those props changes, the component will re-render. That means === equality. For simple primitives, that just means a different value. If we suddenly pass "brown" as the color to our banana component, React will detect that "yellow" !== "brown" and will re-render. For non-primitive values - objects, arrays, functions, class instances, etc. - === checks whether the variables are literally the same variable. That is, when we say

const a = { color: 'yellow' };  
const b = { color: 'yellow' };  

we have two different variable references. The values are the same, but they're different objects stored in different memory references, and therefore a === b is false here. See an introduction to variables if you want to read more about primitive variables.

Functions, like objects, are variable references, which brings us back to the useCallback hook. The problem with React functional components is that scoped functions will always be a different reference and therefore child components will always re-render, which is not performant.

Take for example code like this:

const Input = () => {  
  const [name, setName] = useState('');

  return (
    <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
  );
};

This may look completely innocuous, and in a single isolated component (especially one like this with no children), it is. But the onChange function, as an anonymous function, is always a new reference every time this component renders. Even if you make it not anonymous, because it uses scoped variables and therefore has to be declared inside the component, it will always be a new reference. For example, this doesn't fix it:

const Input = () => {  
  const [name, setName] = useState('');
  const onChange = (e) => setName(e.target.value);

  return <input type="text" value={name} onChange={onChange} />;
};

In a React class component, you wouldn't face this problem because you could make this an instance function and it would therefore always be the same reference. For example, something like this:

class Input extends React.Component {  
  constructor(props) {
    super(props);
    // Here, however, is the tradeoff with class components.
    // They need to be bound to the right context
    // which is a bit of a nuisance.
    this.onChange.bind(this);
    this.state = { name: '' };
  }

  onChange(e) {
    setState({ name: e.target.value });
  }

  // this.onChange always refers to the same function
  // so it won't trigger child re-renders
  render() {
    return <input type="text" value={this.state.name} onChange={this.onChange} />;
  }
}

useCallback exists to solve this problem with functional components. You can pass a function and a list of dependencies to useCallback to create a sort of "cached" function that React will return on future renders so that the function reference doesn't change unnecessarily.

Here's how it works:

const Input = () => {  
  const [name, setName] = useState('');
  const onChange = useCallback((e) => setName(e.target.value), [name]);

  return <input type="text" value={name} onChange={onChange} />;
};

Now that I've written all this using that example . . . it's a pretty terrible example because the second argument to useCallback, the list of dependencies, tells React when to return a new function. If any of the dependencies have different reference equality, React will return a new function. Since name changes every time you call setName, the function returned by useCallback is actually new every time. I'm not changing it though because I think it's a very simple example that illustrates the idea well. But let's look at something more practical too.

Imagine that you have a function that calls an API by dispatching a Redux action, and you need to pass this function down into a form component that puts the data for the call together. And let's say you need to do some level of formatting or validating prior to dispatching the action. If you pass in an anonymous function, the component will re-render every time.

import { callApi } from './actions';

const FormWrapper = () => {  
  const submit = (data) => {
    const name = `${ data.fields.firstname } ${ data.fields.lastname }`;
    const country = data.fields.country || 'USA';
    callApi({ ...data.fields, name, country });
  };

  // "submit" is always a different reference so
  // this component will always re-render
  return <Form submit={submit} />;

But you can use useCallback to create a function that wraps the behavior you need to perform and pass that in to avoid this problem.

import { callApi } from './actions';  
import React, { useCallback } from 'react';

const FormWrapper = () => {  
  const submit = useCallback((data) => {
    const name = `${ data.fields.firstname } ${ data.fields.lastname }`;
    const country = data.fields.country || 'USA';
    callApi({ ...data.fields, name, country });
  }, []);

  // "submit" is cached and since we specified no
  // dependencies, it will never change.
  return <Form submit={submit} />;

As you'll see in some future posts, React gives you a lot of tools for render optimization. But React is actually pretty good at rendering quickly and is already optimized for fast re-rendering, so while it's important to know the tools available to you, the old adage that you shouldn't prematurely optimize is an important one. useCallback can be helpful for creating a function reference that doesn't change from render to render, but you don't need to blindly wrap every function you create. The best thing you can do is understand when and why React will re-render a component because then you can make appropriate choices about when to optimize that re-rendering process.

Thanks for reading!