What is useCallback in React and when to use it?

Viewed 2435

I have gone through a couple of articles on useCallback and useMemo on when to use and when not to use but I have mostly seen very contrived code. I was looking at a code at my company where I have noticed someone have done this:

const takePhoto = useCallback(() => {
    launchCamera({ mediaType: "photo", cameraType: "front" }, onPickImage);
  }, []);

  const pickPhotoFromLibrary = async () => {
    launchImageLibrary({ mediaType: "photo" }, onPickImage);
  }

  const onUploadPress = useCallback(() => {
    Alert.alert(
      "Upload Photo",
      "From where would you like to take your photo?",
      [
        { text: "Camera", onPress: () => takePhoto() },
        { text: "Library", onPress: () => pickPhotoFromLibrary() },
      ]
    );
  }, [pickPhotoFromLibrary, takePhoto]);

This is how onUploadPress is called:

<TouchableOpacity
   style={styles.retakeButton}
   onPress={onUploadPress}
>

Do you think this is the correct way of calling it? Based on my understanding from those articles, this looks in-correct. Can someone tell me when to use useCallback and also maybe explain useCallback in more human terms?

Article I read: When to useMemo and useCallback.

2 Answers

useCallback returns a normal JavaScript function regarding how to use it. It is the same as the one it gets as first parameter regarding what it does. The difference is that this function doesn't get recreated on a new memory reference every time the component re-renders, while a normal function does. It gets recreated on a new reference if one of the variables inside useCalback's dependency array changes.

Now, why you would wanna bother with this? Well It's worth it whenever the normal behaviour of a function is problematic for you. For example if you have that function in the dependency array of an useEffect, or if you pass it down to a component that is memoized with memo.

The callback of an useEffect gets called on the first render and every time one of the variables inside the dependency array changes. And since normally a new version of that function is created on every render, the callback might gets called infinitely. So useCallback is used to memoize it.

A memoized component with memo re-renders only if its state or props changes, not cause its parent re-renders. And since normally a new version of that passed function as props is created, when the parent re-renders, the child component gets a new reference, hence it re-renders. So useCallback is used to memoize it.

To illustrate I created the below working React application. Click on that button to trigger re-renders of the parent and watch the console. Hope it clears things up!

const MemoizedChildWithMemoizedFunctionInProps = React.memo(
  ({ memoizedDummyFunction }) => {
    console.log("MemoizedChildWithMemoizedFunctionInProps renders");
    return <div></div>;
  }
);

const MemoizedChildWithNonMemoizedFunctionInProps = React.memo(
  ({ nonMemoizedDummyFunction }) => {
    console.log("MemoizedChildWithNonMemoizedFunctionInProps renders");
    return <div></div>;
  }
);

const NonMemoizedChild = () => {
  console.log("Non memoized child renders");
  return <div></div>;
};

const Parent = () => {
  const [state, setState] = React.useState(true);

  const nonMemoizedFunction = () => {};
  
  const memoizedFunction = React.useCallback(() => {}, []);
  
  React.useEffect(() => {
    console.log("useEffect callback with nonMemoizedFunction runs");
  }, [nonMemoizedFunction]);

  React.useEffect(() => {
    console.log("useEffect callback with memoizedFunction runs");
  }, [memoizedFunction]);
  
  console.clear();
  console.log("Parent renders");
  
  
  return (
    <div>
      <button onClick={() => setState((prev) => !prev)}>Toggle state</button>
      <MemoizedChildWithMemoizedFunctionInProps
        memoizedFunction={memoizedFunction}
      />
      <MemoizedChildWithNonMemoizedFunctionInProps
        nonMemoizedFunction={nonMemoizedFunction}
      />
      <NonMemoizedChild />
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

It's to know that memoizing is not free, doing it wrong is worse. Here is the most complete and shortest ressource I have ever seen to understand React's render process in depth, to know when it worths memoizing and how doing it properly: React Render Tutorial.

In your case, using useCallback for onUploadPress is a waste, cause a non memoized function, pickPhotoFromLibrary, is in the dependency array. Also it's a waste if TouchableOpacity is not memoized with memo, which I'm not sure it's.

As a side note there is useMemo, which behaves and used like useCallback to memoize non function but referenced values such as objects and arrays for the same reasons, or to memoize any result of a heavy calculation that you don't wanna repeat between renders.

In simple words, useCallback is used to save the function reference somewhere outside the component render so we could use the same reference again. That reference will be changed whenever one of the variables in the dependencies array changes. As you know React try to minimize the re-rendering process by watching some variables' value changes, then it decides to re-render on not depending on the old-value and new-value of those variables. So, the basic usage of useCallback is to hold old-value and the new-value equally.

I will try to demonstrate it more by giving some examples in situations we must use useCalback in.

  • Example 1: When the function is one of the dependencies array of the useEffect.
function Component(){
  const [state, setState] = useState()
  
  // Should use `useCallback`
  function handleChange(input){
    setState(...)
  }

  useEffect(()=>{
    handleChange(...)
  },[handleChange])

  return ...
}
  • Example 2: When the function is being passed to one of the children components. Especially when it is being called on their useEffect hook, it leads to an infinite loop.
function Parent(){
  const [state, setState] = useState()
  
  function handleChange(input){
    setState(...)
  }

  return <Child onChange={handleChange} />
}

function Child({onChange}){
  const [state, setState] = useState()
  
  useEffect(()=>{
    onChange(...)
  },[onChange])

  return "Child"
}
  • Example 3: When you use React Context that holds a state and returns only the state setters functions, you need the consumer of that context to not rerender every time the state update as it may harm the performance.
const Context = React.createContext();

function ContextProvider({children}){
  const [state, setState] = useState([]);
  
  // Should use `useCallback`
  const addToState = (input) => {
    setState(prev => [...prev, input]);
  }

  // Should use `useCallback`
  const removeFromState = (input) => {
    setState(prev => prev.filter(elem => elem.id !== input.id));
  }

  // Should use `useCallback` with empty []
  const getState = () => {
    return state;
  }

  const contextValue= React.useMemo(
    () => ({ addToState , removeFromState , getState}),
    [addToState , removeFromState , getState]
  );

  // if we used `useCallback`, our contextValue will never change and all the subscribers will not re-render
  <Context.Provider value={contextValue}>
    {children}
  </Context.Provider>
}

Example 4: If you are subscribed to the observer, timer, document events, and need to unsubscribe when the component unmount or for any other reason. SO we need to access the same reference to unsubscribe from it.

function Component(){

  // should use `useCallback`
  const handler = () => {...}
  
  useEffect(() => {
    element.addEventListener(eventType, handler)
    return () => element.removeEventListener(eventType, handler)
  }, [eventType, element])


  return ...
}

That's it, there are multiple situations you can use it too, but I hope these examples demonstrated the main idea behind useCallback. And always remember you don't need to use it if the cost of the re-render is negligible.

Related