React's rendering is powerful but can become a performance bottleneck in larger apps. Every state change triggers re-renders across your component tree—sometimes unnecessarily. Enter React.memo and useCallback: your optimization superheroes that prevent wasted renders and keep your app snappy.
Prateek Labroo
Last Updated Feb 4, 2026

Imagine a simple Todo app. When you type in the input field, everything re-renders—even components that haven't changed. Here's why:
Without optimization (problematic version):
// Parent Component - WITHOUT memo/useCallback
function Todo() {
const [newTodo, setNewTodo] = React.useState("");
const [todos, setTodos] = React.useState([]);
// ❌ These functions get recreated on EVERY render
const handleInputChange = (e) => setNewTodo(e.target.value);
const handleAddTodo = () => {
// add todo logic
};
console.log("Parent renders"); // Logs constantly!
return (
<>
<TodoInput
newTodo={newTodo}
handleInputChange={handleInputChange}
handleAddTodo={handleAddTodo}
/>
<TodoList todos={todos} /* other handlers */ />
</>
);
}
// Child components - even with memo, they re-render!
const TodoInput = React.memo(({ newTodo, handleInputChange }) => {
console.log("TodoInput re-rendered!"); // Still logs!
return <input value={newTodo} onChange={handleInputChange} />;
});
Parent renders TodoInput re-rendered! TodoList re-rendered! // Even worse with 100+ todos
Why this happens:
handleInputChange is a new function reference every render
React.memo does shallow prop comparison
New function = "props changed" = re-render everything
memo + useCallback MagicHere's the optimized version that stops unnecessary renders:
import React, { useCallback, useState, useEffect } from "react";
import TodoInput from "./TodoInput";
import TodoList from "./TodoList";
export default function Todo() {
const [newTodo, setNewTodo] = useState("");
const [todos, setTodos] = useState(
JSON.parse(localStorage.getItem("todos")) || []
);
// ✅ useCallback prevents function recreation
const handleInputChange = useCallback((e) => {
setNewTodo(e.target.value);
}, []);
const handleAddTodo = useCallback(() => {
if (newTodo.trim() !== "") {
const newTodoItem = {
id: Date.now(),
text: newTodo,
completed: false,
edit: false,
};
setTodos((prevTodos) => [...prevTodos, newTodoItem]);
setNewTodo("");
}
}, [newTodo]);
const handleEdit = useCallback((id) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, edit: !todo.edit } : todo
)
);
}, []);
const handleDelete = useCallback((id) => {
setTodos((prevState) => prevState.filter((todo) => todo.id !== id));
}, []);
const handleComplete = useCallback((id) => {
setTodos((prevState) =>
prevState.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const handleEditedTodo = useCallback((e, id) => {
setTodos((prevState) =>
prevState.map((todo) =>
todo.id === id ? { ...todo, text: e.target.value } : todo
)
);
}, []);
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
console.log("Parent"); // Only logs when todos change
return (
<div className="container">
<h1 className="heading">Todo Component</h1>
<div className="flex flex-col mt-4 mx-auto p-4 border rounded min-w-[650px] min-h-[600px]">
<TodoInput
newTodo={newTodo}
handleInputChange={handleInputChange}
handleKeyDown={handleAddTodo} // Stable reference
handleAddTodo={handleAddTodo}
/>
<TodoList
todos={todos}
handleEdit={handleEdit}
handleDelete={handleDelete}
handleComplete={handleComplete}
handleEditedTodo={handleEditedTodo}
/>
</div>
</div>
);
}
// TodoInput.jsx - ✅ memoized
import React, { memo } from "react";
export default memo(function TodoInput({
newTodo,
handleInputChange,
handleKeyDown,
handleAddTodo,
}) {
console.log("TodoInput"); // Only logs when newTodo changes
return (
<div className="flex border-gray-300 border-b pb-3">
<input
type="text"
className="border border-gray-300 rounded-lg p-2 flex-grow mr-2"
placeholder="Add a new todo"
value={newTodo}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<button
onClick={handleAddTodo}
className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 cursor-pointer"
>
Add
</button>
</div>
);
});
// TodoList.jsx - ✅ memoized
import { memo } from "react";
export default memo(function TodoList({
todos,
handleEdit,
handleDelete,
handleComplete,
handleEditedTodo,
}) {
console.log("TodoList"); // Only logs when todos array changes
return (
<div className="flex flex-col gap-4 mt-3 overflow-auto flex-grow">
{todos.map((todo) => (
<div key={todo.id} className="flex justify-start p-2 items-center">
{todo.edit ? (
<input
type="text"
value={todo.text}
className="border border-gray-300 rounded-lg p-2 flex-grow mr-2"
onChange={(e) => handleEditedTodo(e, todo.id)}
/>
) : (
<span
className={`self-start border border-gray-300 rounded-lg p-2 flex-grow mr-3 text-wrap transition-all ease-in-out ${
todo.completed
? "line-through text-red-500"
: "text-teal-600 underline"
}`}
>
{todo.text}
</span>
)}
<div className="flex gap-2">
<button
className="text-blue-500 hover:text-blue-700 cursor-pointer"
onClick={() => handleEdit(todo.id)}
>
{todo.edit ? "Save" : "Edit"}
</button>
<button
className="text-red-500 hover:text-red-700 cursor-pointer"
onClick={() => handleDelete(todo.id)}
>
Delete
</button>
<button
className="text-green-500 hover:text-green-700 cursor-pointer"
onClick={() => handleComplete(todo.id)}
>
Complete
</button>
</div>
</div>
))}
</div>
);
});
| Scenario | Without Optimization | With memo + useCallback |
|---|---|---|
| Typing in input | Parent + Both children re-render | Only TodoInput |
| Adding todo | All re-render | Parent + TodoList |
| Editing todo #5 | All re-render | Only TodoList |
| 100 todos + typing | 300ms lag | <10ms |
✅ Memoize functions passed as props
✅ Empty deps [] for functions without state deps
✅ Include changing state in deps array
❌ Don't overdo it - small apps might not need it
React.memo Rules✅ Use for pure components with stable props
✅ Great for lists/item components
✅ Won't help if props keep changing
// 1. useCallback for event handlers const handleClick = useCallback((id) => {}, [id]);
// 2. Memoize objects/arrays passed as props const config = useMemo(() => ({ limit: 10, sort: 'asc' }), []);
// 3. Custom comparison for memo const MyComponent = memo(Component, (prev, next) => { return prev.count === next.count; // Deep compare if needed });
Todo App with 500 items:
✅ Improvement: 20x faster!
❌ Without optimization: 245ms per keystroke
✅ With memo + useCallback: 12ms per keystroke
Bottom line: For lists, forms, and interactive UIs, memo + useCallback = must-have optimization. Your users will notice the difference!
Be the first to share your thoughts!
No comments yet.
Start the conversation!
Build Your Portfolio
Help the Community
Strengthen Your Skills
Share your knowledge by writing a blog or quick notes. Your contribution can help thousands of frontend developers ace their interviews and grow their careers! 🚀
Anuj Sharma
Last Updated Oct 2, 2025
Explore Polyfill for map, filter and reduce array methods in JavaScript. A detailed explanation of Map, filter and reduce polyfills in JS helps you to know the internal working of these array methods.
Anuj Sharma
Last Updated Nov 24, 2025
Understand step by step how to flatten nested array in javascript using recursion, also explore the flatten of complex array of object.
Anuj Sharma
Last Updated Nov 23, 2025
Find the step-by-step explanation of the useFetch custom hook in React that helps in fetching the data from an API and handling loading, error states.
Alok Kumar Giri
Last Updated Jun 2, 2025
Code snippet examples which will help to grasp the concept of Hoisting in JavaScript, with solutions to understand how it works behind the scene.
Anuj Sharma
Last Updated Aug 3, 2025
Explore the implementation of setTimeout in JavaScript with a detailed explanation for every step. Understand all scenarios expected to implement the setTimeout polyfill.
Anuj Sharma
Last Updated Oct 26, 2025
Understand the step-by-step implementation of Infinite Currying Multiplication in JavaScript with a code example.
Subscribe to FrontendGeek Hub for frontend interview preparation, interview experiences, curated resources and roadmaps.
All in One Preparation Hub to Ace Frontend Interviews. Master JavaScript, React, System Design, and more with curated resources.
© 2026 FrontendGeek. All rights reserved