Understanding Function Call vs Function Reference in React: A Deep Dive

React's event handling and function calls often leave developers with confusion regarding the performance implications of certain practices. In this article, we will explore the difference between function calls and function references, specifically focusing on event handling in React. We will cover common scenarios, the importance of efficient function handling, and the use of currying for optimizing performance. Let’s break down the concepts using concrete code examples and analyze why certain practices are better than others.


The Setup: Understanding the Context

Let’s start with a simple example where we have a Button component that calls an event handler (handleEdit or handleDelete) when clicked. Initially, the event handlers are passed directly inside the onClick props using arrow functions.

Here’s the code:

import React from "react";
import Button from "../Button/Button";

function ToDoItem({ item, onDelete, onEdit }) {
  return (
    <div>
      <span>{item.todo}</span>
      <Button
        label="Edit"
        onClick={() => onEdit(item.id)}  {/* Inline function call */}
      />
      <Button
        label="Delete"
        onClick={() => onDelete(item.id)}  {/* Inline function call */}
      />
    </div>
  );
}

Now, let’s focus on the key question: Why does React re-render when we pass these arrow functions? And what’s the difference between using a function call vs. a function reference?

Function Call vs. Function Reference

In React, there is an important distinction between a function call and a function reference.

  • Function Reference: This is when you pass a reference to the function itself, meaning you don’t immediately invoke the function. React can then call the function later when the event occurs.

    Example:

<Button onClick={onEdit} />
  • Function Call: This occurs when you invoke the function immediately. When you pass a function call as a prop, React doesn't just pass a reference to the function; instead, it executes the function immediately during the render phase.

Example:

<Button onClick={() => onEdit(item.id)} />

Why Does This Matter?

The issue with inline function calls (e.g., onClick={() => onEdit(item.id)}) is that React will create a new function on each render. This is because arrow functions are considered new functions each time the component re-renders. React cannot properly optimize for this because it treats every render as a separate instance of that function.

How React Diffing Works

React uses a virtual DOM diffing algorithm to compare previous and current renders to figure out which parts of the UI need to be updated. If the same function is passed multiple times, React can quickly recognize that nothing has changed and avoid unnecessary re-renders. However, when you use inline functions, React thinks that the function has changed every time, triggering unnecessary renders even if the logic hasn’t changed.

The Issue in Action: Button Event Handlers

Let’s say you have an array of todos, and each todo item has an edit and delete button. In the code below, we pass inline arrow functions to the Button component:

import React, { useState } from "react";
import Button from "./Button";

function ToDos({ todos = [], onDelete, onEdit }) {
  return (
    <div>
      {todos.map((item) => (
        <ToDoItem
          key={item.id}
          item={item}
          onDelete={onDelete}
          onEdit={onEdit}
        />
      ))}
    </div>
  );
}

function ToDoItem({ item, onDelete, onEdit }) {
  return (
    <div>
      <span>{item.todo}</span>
      <Button
        label="Edit"
        onClick={() => onEdit(item.id)}  {/* Function call */}
      />
      <Button
        label="Delete"
        onClick={() => onDelete(item.id)}  {/* Function call */}
      />
    </div>
  );
}

In this case, every re-render of ToDoItem will recreate the onClick function. This can be inefficient if you have a large number of items.

The Solution: Using Function References

Instead of passing a function call inline, you can pass a function reference that doesn’t create a new function on each render. A better approach is to pre-bind the arguments inside the parent component before passing them to the child components.

Here’s how to refactor the code using function references:

Refactored Code Using Function References:

function ToDoItem({ item, onDelete, onEdit }) {
  const handleEditClick = () => onEdit(item.id);
  const handleDeleteClick = () => onDelete(item.id);

  return (
    <div>
      <span>{item.todo}</span>
      <Button label="Edit" onClick={handleEditClick} />
      <Button label="Delete" onClick={handleDeleteClick} />
    </div>
  );
}

Now, React only passes the references to the handleEditClick and handleDeleteClick functions, rather than creating new inline arrow functions every time. This will allow React’s diffing algorithm to optimize the renders better, resulting in fewer unnecessary re-renders.


The Curried Function Approach

Another option to optimize this situation is using currying. This approach creates a function that accepts arguments upfront and returns another function that can be called later. In the context of event handlers, currying can help you avoid creating new functions on every render while still being able to pass dynamic parameters.

Curried Function Example:

Here’s how you could rewrite the event handler using currying:

function createHandler(fn, id) {
  return () => fn(id);
}

function ToDoItem({ item, onDelete, onEdit }) {
  return (
    <div>
      <span>{item.todo}</span>
      <Button label="Edit" onClick={createHandler(onEdit, item.id)} />
      <Button label="Delete" onClick={createHandler(onDelete, item.id)} />
    </div>
  );
}

Why Currying?

  1. Performance: Currying allows you to define a handler once and reuse it. No need to define a new function on every render.

  2. Readability: It separates the concerns—event handling is clearly managed outside the JSX.

  3. Reusability: You can reuse the same curried handler across different components.

The handleAddTodo Confusion

Now, let’s discuss another case where handleAddTodo doesn’t use an arrow function. You might wonder why we didn’t use an arrow function for handleAddTodo in this case, especially when React typically expects handlers to be passed in as functions.

Here’s the code:

function handleAddTodo() {
  const newTodo = { id: window.todoId++, todo: todoToAdd };
  setTodos([newTodo, ...todos]);
  setTodoToAdd("");  // Clear the input field
}

<Button label="Add todo" onClick={handleAddTodo} />

Why is this different from the onClick={() => handleAddTodo()} approach?

In this case, we are not passing inline arrow functions, but a function reference. This works because handleAddTodo doesn’t need any additional arguments when it’s called directly by the Button component. React will execute handleAddTodo when the button is clicked, and it won’t create a new function each time.

If you used onClick={() => handleAddTodo()}, React would create a new function every render, even though no extra parameters are involved.

Conclusion: Best Practices

  1. Function Reference over Function Call: Always prefer passing function references instead of inline function calls to avoid unnecessary re-renders.

  2. Currying: For better performance, reuse functions by passing arguments upfront with currying.

  3. Reusability and Performance: Whether you use a reference or currying, make sure to optimize for reusability and performance, especially in larger applications.

By understanding these concepts, you can make more informed decisions about how to structure your event handlers in React and avoid common pitfalls that can hurt performance.