Uncategorized

Step 7 : “Why is My React App Slow? A Beginner’s Guide to memo, useCallback, and useMemo”

This step is where you go from a “working” app to a “fast” and “professional” app. This is an intermediate-to-advanced topic, but it’s one of the most important for building high-quality applications.

Goal: Understand why React apps can become slow and learn the specific tools to fix it by preventing unnecessary re-renders.

The tools are: React.memo, useCallback, and useMemo.


1. 🧠 The Core Problem: Unnecessary Re-Renders

First, you must understand why a component re-renders. A component re-renders if:

  1. Its state changes (e.g., you call setCount(1)).
  2. Its props change.
  3. Its parent component re-renders.

This last one is the problem.

Imagine your App component has a timer state that updates every second.

App
 ├── Timer (displays the time)
 └── TodoList
      ├── TodoItem (id: 1)
      ├── TodoItem (id: 2)
      └── TodoItem (id: 3)

When the timer state updates in App, App re-renders. Because App is the parent of TodoList, TodoList also re-renders. And because TodoList is the parent of all the TodoItems, every single TodoItem re-renders.

This means your TodoItems are re-rendering every second, even though their props (todo.text, etc.) haven’t changed at all. This is a wasted render. On a big app, this can cause significant lag.

Our goal is to tell React: “Hey, TodoItem, if your props haven’t actually changed, just skip re-rendering and show your old self.”


2. 🧠 Solution 1: React.memo() (For Components)

React.memo is a Higher-Order Component (a function that wraps your component). It memoizes (remembers) the rendered output of your component.

It tells React: “Before you re-render this component, do a shallow comparison of its newProps and oldProps. If they are identical, skip the render and use the memoized (cached) version.”

How to use it:

You just wrap your component when you export it.

JavaScript

// src/TodoItem.jsx

// 1. Your component
function TodoItem({ todo }) {
  // 2. Add a log to see when it renders
  console.log(`Rendering TodoItem ${todo.id}`);
  
  return (
    <li className="todo-item">{todo.text}</li>
  );
}

// 3. Wrap it in React.memo
export default React.memo(TodoItem);

By doing this, when the App timer ticks, React.memo will check the todo prop. It will see oldProps.todo is the same object as newProps.todo, and it will skip the render.


3. 🧠 The “Gotcha”: Referential Equality (Why React.memo Fails)

This is the hard part. After you do Step 2, you might find your component still re-renders. Why?

Let’s say your TodoItem also takes a function prop, onDelete: <TodoItem todo={todo} onDelete={handleDeleteTodo} />

In your App component, that function looks like this:

JavaScript

function App() {
  // ... other state
  
  // This function is RE-CREATED every time App re-renders
  function handleDeleteTodo(id) {
    setTodos(prev => prev.filter(t => t.id !== id));
  }
  
  // ...
  return <TodoList ... onDelete={handleDeleteTodo} />
}

In JavaScript, functions and objects are not equal to each other just because they look the same.

  • 5 === 5 (true)
  • 'a' === 'a' (true)
  • {} === {} (false! These are two different objects in memory)
  • () => {} === () => {} (false! These are two different functions in memory)

When App re-renders every second, it re-creates the handleDeleteTodo function. React.memo compares the oldProps.onDelete (the old function) with newProps.onDelete (the new function) and sees they are different references. So, it re-renders.

We need a way to tell React: “Don’t re-create this function. Give me the exact same one as last time.”


4. 🧠 Solution 2: useCallback() (For Functions)

The useCallback hook memoizes a function definition. It gives you back the exact same function reference on every render, as long as its dependencies haven’t changed.

How to use it:

You wrap your function definition in useCallback.

JavaScript

// src/App.jsx
import { useState, useCallback } from 'react';

function App() {
  // ...
  
  // Wrap the function in useCallback
  const handleDeleteTodo = useCallback((id) => {
    // We use the functional update form for setTodos
    // This lets us avoid adding 'todos' to the dependency array
    setTodos(prevTodos => prevTodos.filter(t => t.id !== id));
  }, []); // <-- The dependency array is empty
  
  // ...
  return <TodoList ... onDelete={handleDeleteTodo} />
}

Now, when App re-renders, handleDeleteTodo is not re-created. It’s the exact same function as the last render. React.memo on the TodoItem will compare the onDelete prop, see that it’s identical, and successfully skip the re-render.


5. 🧠 Solution 3: useMemo() (For Values)

useMemo is just like useCallback, but it’s for memoizing complex values (like objects or arrays) or the result of an expensive calculation.

  • useCallback(fn, deps) is the same as useMemo(() => fn, deps).

Use Case 1: Expensive Calculation Imagine you have 10,000 todos and want to show a count of only the completed ones.

JavaScript

// This calculation is SLOW
function getCompletedCount(todos) {
  console.log("Calculating completed todos...");
  return todos.filter(t => t.isComplete).length;
}

function TodoList({ todos }) {
  // This runs on EVERY re-render (e.g., when you type in a new-todo box)
  const completedCount = getCompletedCount(todos); 
  // ...
}

This is wasteful. We can use useMemo to cache the result.

JavaScript

import { useMemo } from 'react';

function TodoList({ todos }) {
  // This function will ONLY re-run if the 'todos' array changes
  const completedCount = useMemo(() => {
    console.log("Calculating completed todos...");
    return todos.filter(t => t.isComplete).length;
  }, [todos]); // <-- Dependency
  
  return <h2>Completed: {completedCount}</h2>
}

Use Case 2: Referential Equality (for objects) If you pass an object as a prop, you have the same problem as functions. <ChildComponent style={{ color: 'red' }} /> That style object is re-created every time. React.memo(ChildComponent) will fail. Fix: const memoizedStyle = useMemo(() => ({ color: 'red' }), []); <ChildComponent style={memoizedStyle} />


2. 🚀 Demo Project: Optimized To-Do List

Let’s build the demo that shows the problem.

Coding Steps:

Step 1: Create the “Problem” App Use your To-Do list from Step 3, but add a timer to App.jsx.

JavaScript

// src/App.jsx
import { useState, useEffect } from 'react';
import TodoList from './TodoList'; // A component that just maps todos
import TodoForm from './TodoForm'; // The form from Step 3

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Item 1" },
    { id: 2, text: "Item 2" }
  ]);
  
  // THE "NOISY NEIGHBOR" - Our timer
  const [time, setTime] =useState(new Date());
  useEffect(() => {
    const timerId = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(timerId);
  }, []);

  // This function is re-created every second
  function handleDeleteTodo(id) {
    setTodos(prev => prev.filter(t => t.id !== id));
  }
  
  return (
    <div>
      <h1>Time: {time.toLocaleTimeString()}</h1>
      <TodoForm />
      <TodoList todos={todos} onDelete={handleDeleteTodo} />
    </div>
  );
}

// src/TodoList.jsx
import TodoItem from './TodoItem';
export default function TodoList({ todos, onDelete }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onDelete={onDelete} />
      ))}
    </ul>
  );
}

// src/TodoItem.jsx
function TodoItem({ todo, onDelete }) {
  // This will log every second for every item!
  console.log(`Rendering Item: ${todo.text}`); 
  return (
    <li>
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
}
// export default TodoItem; // <-- Not memoized yet

Observation: Open your console. You’ll see Rendering Item: Item 1 and Rendering Item: Item 2 logged every single second. This is the problem.

Step 2: Fix the Problem

  1. Memoize TodoItem:JavaScript :
    • Observation: Check the console. It’s still logging every second. Why? Because the onDelete prop is a new function every time.
  2. Memoize handleDeleteTodo:JavaScript
// src/App.jsx
import { useState, useEffect, useCallback } from 'react'; // <-- Add useCallback

// ...
const handleDeleteTodo = useCallback((id) => { // <-- FIX 2
  setTodos(prev => prev.filter(t => t.id !== id));
}, []); // <-- Empty dependency array
// ...

Final Observation: Check your console. It’s silent. The items are no longer re-rendering. They only log to the console when you add or delete a todo. You have successfully optimized your app.

⚠️ Important Rule: Don’t memoize everything. This is for fixing performance problems, not for using on every component. React.memo and useCallback have a small cost. Use them when you can measure a performance problem.

Leave a Reply

Your email address will not be published. Required fields are marked *