Search code examples
javascriptreactjstypescriptreact-native

What is useCallback in React and when to use it?


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 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 of those articles, this looks incorrect. 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.


Solution

  • useCallback accepts as a first parameter a function and returns a memoized version of it (in terms of its memory location, not the computation done inside). Meaning that the returned function doesn't get recreated on a new memory reference every time the component re-renders, while a normal function inside a component does.

    The returned function gets recreated on a new memory reference if one of the variables inside useCallback's dependency array (its second parameter) changes.

    Now, why would you wanna bother with this? Well, It's worth it whenever the normal behavior of a function inside a component 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 its dependency array changes. And since normally a new version of that function is created on every render, the callback might get called infinitely. So useCallback is used to memoize it.

    A memoized component with memo re-renders only if its state or props changes, not because 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, so doing it wrong is worse. In your case, using useCallback for onUploadPress is a waste because 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 is 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.

    A great resource to understand React render process in depth to know when to memoize and how to do it well: React Render.