React Native useCallback, When and How To Use It?

·

9 min read

This blog will be heavily influenced by react's official documents,(one might say: with a dependency array of [react] ) because I think they are great. I will try to simplify, shorten it and adding React Native specific examples. I will also add and summarize the references on the documents to try to make it a comprehensive summary. Firs section under 'useCallback What Is' will be about the usage of useCallback and will be sufficient enough to learn and use it; 'Deep Dive' section will show some of the best practices of useCallback along with useEffect.

useCallback, What Is?

lets you cache a function definition between re-renders.


FlashInfo**

**no, this blog isn't sponsored by shopify's FlashList but you may want to check it out.

useCallback -> caches the function itself.

useMemo -> Calls your function and caches its result.

On the initial render, the returned function you’ll get from useCallback will be the function you passed.

On the following renders, React will compare the dependencies with the dependencies you passed during the previous render. If none of the dependencies have changed (compared with 'Object.is*'), useCallback will return the same function as before. Otherwise, useCallback will return the function you passed with changed dependencies.

Object.is: In JavaScript, a function () {} or () => {} always creates a different function, which means handleSubmit will never be the same; ShippingForm will have different prop(onSubmit) each time and memo you just did will have no meaning. By wrapping handleSubmit in useCallback, you ensure that it’s the same function between the re-renders (until dependencies change).

Note: By default, when a component re-renders, React re-renders all of its children recursively. Meaning that it calls itself again to repeat the code.

FlashExample:

You are making an app to Call a taxi(maybe you're a little late to the trend). You're writing CallACab that renders a RequestForm in it:

function CallACab({ userId, referrer, theme }) {
  const requirements = useMemo(() => { // Calls your function and caches its result
    const position = X;
    return computeRequirements(position);
  }, [position]); // as long as position doesn't change

  // Tell React to cache your function between re-renders...
  const handleSubmit = useCallback(
    (requestDetails) => {
      post("/user/" + userId + "/buy", {
        referrer,
        requestDetails,
      });
    },
    [userId, referrer] // as long as these dependencies don't change...
  );

  return (
    <View style={{ backgroundColor: theme }}>
      {/* ...RequestForm will receive the same props and can skip re-rendering */}
      <RequestForm requirements={requirements} onSubmit={handleSubmit} />
    </View>
  );
}

export default CallACab;

What might have been?

  • If you wouldn't have used useCallback on handleSubmit everytime the theme changed JavaScript would have created* a new handleSubmit and since ShippingForm's props changed, it would also be rendered again.

    Note: useCallbackdoes not prevent creating the function. You’re always creating a function (and that’s fine!), but React ignores it and gives you back your cached function so long the dependencies are the same.

  • Same principle applies to requirements too. Bu you only cached its return value.

    Note: The function you wrap in useMemo runs during rendering, so this only works for pure functions. -< I'll explain pure functions briefly in a moment.


Places where caching a function with useCallback is only valuable:

  • You pass it as a prop to a component wrapped in memo. E.g. from above:
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});
  • The function you’re passing is later used as a dependency of some Hook. For example, your 'useEffect' uses this function as a dependency.
function ChatRoom({ roomId }) {
    const [message, setMessage] = useState('');

    const createOptions = useCallback(() => {
      return {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
    }, [roomId]); 

    useEffect(() => {
      const options = createOptions();
      const connection = createConnection();
      connection.connect();
      return () => connection.disconnect();
    }, [createOptions]); 
    // ...

Above code is a good example of useCallBack with correct dependencies, However, it’s even better to remove the need for a function dependency. You should move your function inside the Effect:

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() { // ✅ No need for useCallback or function dependencies!
      return {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ Only changes when roomId changes
  // ...

Diving Deeper

NotSoFlashInfo

In practice, you can make a lot of memoization unnecessary by following a few principles:

  1. When a component visually wraps other components, let it accept JSX as children.* This way, when the wrapper component updates its own state, React knows that its children don’t need to re-render.

    • *accept JSX as children

    Here, let’s say you defined 2 components: Card and Avatar. Card accepts Avatar(JSX) as a children. Or Card wraps Avatar; think of it like a frame:

    callBack.png

         <View>
             <Card>
                 <Avatar />
             </Card>
         </View>
    

    When you nest content inside a JSX tag, the parent component will receive that content in a prop called children. For example, the Cardcomponent above will receive a childrenprop set to <Avatar />and render it in a wrapper View. Or you can insert Text component instead of Avatar:

    text6.png

    You can think of a component with a 'children' prop as having a “hole” that can be “filled in” by its parent components with arbitrary JSX: image.png

  2. Prefer local state and don’t lift state up* any further than necessary. For example, don’t keep short lasting states like forms in a global state library.**

    • *lift state up:

      when you move a state up to the parent component and control child’s state from parent.

    • **a tip: let’s say you want to keep track of tweetCount in your redux store. const {tweetCount} useSelector(state ⇒ state.tweets) isn’t a correct way to track it. Because the state tracks whole of tweets so every time it renders whole tweet state gets rendered. That’s why you should: const tweetCount = useSelector(state ⇒ state.tweets.tweetCount);

  3. Keep your rendering logic pure. (remember I tould you about pure functions) If re-rendering a component causes a problem or produces some noticeable visual artifact, it’s a bug in your component! Fix the bug instead of adding memoization.

    • *pure functions

      a pure functions is a function with the following characteristics:

      • It minds its own business. It does not change any objects or variables that existed before it was called. So it doesn't have an evil plan to make our timeline the darkest one.
      • Same inputs, same output. Think of it like 2x = y. Everytime you insert 2 into x you get 4 from y.

      Note: React is designed around this concept. React assumes that every component you write is a pure function.

      All in all pure functions don’t mutate(change) variables outside of their scope or objects that were created before the call.

      Important: Updating the screen, starting an animation, changing the data—are called side effects. They’re not happening during the rendering, even though event handlers are defined inside your component, event handlers don’t need to be pure.

    Writing pure functions takes a bit of practice, but it unlocks the power of React’s paradigm.

  4. Avoid unnecessary Effects that update state.* Most performance problems in React apps are caused by chains of updates originating from Effects that cause your components to render over and over.

    • *unnecessary Effects

      *this section would be too deep if we go into every detail, if you want that you can click the link above, I'll try to summarize it.

      Effects are an escape door from the React paradigm. They let you “step outside” of React and synchronize your components with some external system.

      There are two common cases in which you don’t need Effects:

      • You don’t need Effects to transform data for rendering. filtering a list with a state inside useEffect.
      • You don’t need Effects to handle user events. handleSubmit etc...

      Don't use useEffect when you can Cache

      
      function TodoList({ todos, filter }) {
       const [newTodo, setNewTodo] = useState('');
      
       // 🔴 Avoid: redundant state and unnecessary Effect
       const [visibleTodos, setVisibleTodos] = useState([]);
       useEffect(() => {
         setVisibleTodos(getFilteredTodos(todos, filter));
       }, [todos, filter]);
      
       // ...
      } 
      
      function TodoList({ todos, filter }) {
       const [newTodo, setNewTodo] = useState('');
       // ✅ instead you can cache the function
       const visibleTodos = useCallback(() => {  
         // your function here
       }, [todos, filter]);
       // ...
      }
      

      FlashTip

      When you choose whether to put some logic into an event handler or an Effect, you need to think from the user’s perspective. If something is caused by a particular interaction, use an event handler(handleCallPress, onPress). If it’s caused by the user seeing the component on the screen, use-Effect.


      useEffect that runs only once on first render

      In other words useEffect with no dependency array.

      In general, your components should be resilient to being remounted. Although it may not ever get remounted in practice, following the same constraints in all components makes it easier to move and reuse code. The correct usage of no depency array is:

      
      let didInit = false;
      
      function App() {
       useEffect(() => {
         if (!didInit) {
           didInit = true;
           // ✅ Only runs once per app load
           loadDataFromLocalStorage();
           checkAuthToken();
         }
       }, []);
       // ...
      }
      

      FlashTip

      Whenever you try to keep two different state variables synchronized, it’s a sign to try lifting state up instead!


      Passing data to the parent

      Since both the child and the parent component need the same data, let the parent component fetch that data, and pass it down to the child instead:

      function Parent() {
       const data = useSomeAPI();
       // ...
       // ✅ Good: Passing data down to the child
       return <Child data={data} />;
      }
      
      function Child({ data }) {
       // ...
      }
      

      Subscribing to an external store

      Although it’s common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead. Delete the Effect and replace it with a call to useSyncExternalStore :

      Recap

      • If you can calculate something during render, don't use Effect.
      • To cache expensive calculations, add useMemo or useCallback instead of useEffect.
      • Only code that needs to run because a component was seen by the user should be in Effects.
      • Whenever you try to synchronize state variables in different components, consider lifting state up.

Updating state based on the previous state

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
  // ...

You can remove [todos] dependency by passing an updater function instead.

Updater function takes the pending state and calculates the next state from it:

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos(todos => [...todos, newTodo]);
  }, []); // ✅ No need for the todos dependency because we passed an updater function
  // ...

One Last Tip: Optimizing a custom Hook

If you’re writing a custom Hook, it’s recommended to wrap any functions that it returns into useCallback. This ensures that the consumers of your Hook can optimize their own code when needed:

End

Thank you for reading and I'm happy to receive any type of feedback.