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?
Performance: Currying allows you to define a handler once and reuse it. No need to define a new function on every render.
Readability: It separates the concerns—event handling is clearly managed outside the JSX.
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
Function Reference over Function Call: Always prefer passing function references instead of inline function calls to avoid unnecessary re-renders.
Currying: For better performance, reuse functions by passing arguments upfront with currying.
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.