React Performance Hooks

React is designed to make building user interfaces easier, but as applications become larger, performance can become an important consideration.

A page may contain:

  • Large lists
  • Complex calculations
  • Expensive filtering or sorting
  • Many interactive components
  • Large forms
  • Charts and dashboards
  • Animations
  • Search interfaces
  • Frequently changing state

Most React applications perform well without manually optimizing every component. However, some situations can cause unnecessary work.

React provides several Hooks and APIs that can help manage performance:

  • useMemo
  • useCallback
  • useTransition
  • useDeferredValue

These tools solve different problems.

useMemo and useCallback are primarily related to memoization. They can help avoid unnecessary calculations or preserve the identity of values and functions when that identity matters.

useTransition and useDeferredValue are related to responsiveness. They allow React to treat certain updates as less urgent so that important interactions can remain responsive.

The goal is not to make every component use performance Hooks.

The goal is to understand where the expensive work is, why it is happening, and which optimization actually addresses it.

What Is React Performance?

Performance in a React application is largely about how much work React and the browser need to perform to keep the interface responsive.

For example, imagine a search page containing 10,000 products.

When the user types:

laptop

the application might need to:

  1. Update the input.
  2. Filter thousands of products.
  3. Render the matching results.
  4. Update the browser’s DOM.

If filtering and rendering are expensive, the input may feel slow.

Performance optimization attempts to reduce unnecessary work or make expensive work less disruptive to user interactions.

What Is Memoization?

Memoization is an optimization technique where a previously calculated result is remembered and reused when the relevant inputs have not changed.

For example, imagine this calculation:

const result = calculateSomething(a, b);

If calculateSomething() is expensive and a and b have not changed, there may be no reason to calculate the same result again.

Memoization can remember the previous result.

Conceptually:

Inputs
  ↓
Calculation
  ↓
Result
  ↓
Remember result

On a later render:

Same inputs?
   ↓
Yes
   ↓
Reuse result

React provides useMemo for this purpose.

useMemo

useMemo lets you cache the result of a calculation between renders.

Import it from React:

import { useMemo } from "react";

The basic syntax is:

const cachedValue = useMemo(() => {
  return calculateValue();
}, [dependencies]);

React can reuse the calculated value when the dependencies have not changed.

Basic useMemo Example

Suppose you have a product list:

function ProductList({ products, category }) {
  const filteredProducts = useMemo(() => {
    return products.filter(
      (product) => product.category === category
    );
  }, [products, category]);

  return (
    <ul>
      {filteredProducts.map((product) => (
        <li key={product.id}>
          {product.name}
        </li>
      ))}
    </ul>
  );
}

The calculation depends on:

products
category

Therefore:

[products, category]

are the dependencies.

If another unrelated value changes, React can reuse the previous filtered result.

What useMemo Does Not Do

useMemo does not automatically make an entire component faster.

It only memoizes the value returned by its calculation.

For example:

const total = useMemo(() => {
  return items.reduce(
    (sum, item) => sum + item.price,
    0
  );
}, [items]);

This does not prevent the component from rendering.

It only caches the result of the calculation.

This distinction is important.

useMemo With Expensive Calculations

Memoization becomes more useful when a calculation is genuinely expensive.

For example:

const sortedProducts = useMemo(() => {
  return [...products].sort(
    (a, b) => a.price - b.price
  );
}, [products]);

Notice:

[...products]

is used before sort().

That is important because sort() mutates the array it operates on.

Creating a copy prevents the original products array from being mutated.

If the environment supports it, toSorted() can also be used:

const sortedProducts = useMemo(() => {
  return products.toSorted(
    (a, b) => a.price - b.price
  );
}, [products]);

useMemo With Derived Data

Sometimes a component has expensive derived data.

For example:

const statistics = useMemo(() => {
  return calculateStatistics(data);
}, [data]);

If data has not changed, React can reuse the previous result.

However, for simple calculations such as:

const count = products.length;

useMemo is usually unnecessary.

The calculation is already extremely cheap.

useCallback

useCallback is used to cache a function definition between renders.

Import it:

import { useCallback } from "react";

The basic syntax is:

const handleClick = useCallback(() => {
  // function logic
}, [dependencies]);

React can return the same function identity between renders when the dependencies have not changed.

Why Function Identity Matters

In JavaScript, creating a function produces a function object with its own identity.

Consider:

function Parent() {
  function handleClick() {
    console.log("Clicked");
  }

  return <Child onClick={handleClick} />;
}

On another render, the handleClick function is created again.

Even though the code looks identical, it is a different function object.

This can matter when passing functions to memoized child components or when a function is itself used as a dependency.

useCallback Example

function SearchPage() {
  const [query, setQuery] = useState("");

  const handleSearch = useCallback(() => {
    console.log("Searching for:", query);
  }, [query]);

  return (
    <SearchButton onSearch={handleSearch} />
  );
}

The callback depends on:

query

Therefore it belongs in the dependency array.

useCallback and memo

useCallback is often discussed together with memo.

Consider:

const SearchButton = memo(function SearchButton({
  onSearch,
}) {
  console.log("SearchButton rendered");

  return (
    <button onClick={onSearch}>
      Search
    </button>
  );
});

Now the parent can use:

const handleSearch = useCallback(() => {
  console.log("Search");
}, []);

If the parent re-renders for another reason, the callback can retain the same identity.

That can allow memo to skip an unnecessary child render when the child’s other props have also remained the same.

useCallback Does Not Make Functions Faster

This is a common misunderstanding.

useCallback does not make the function’s internal logic execute faster.

For example:

const calculate = useCallback(() => {
  return expensiveCalculation();
}, []);

The calculation itself has not become faster.

useCallback primarily preserves the function identity.

If you want to cache a calculation’s result, useMemo is the relevant tool.

useMemo vs useCallback

The difference can be summarized simply:

useMemo
   ↓
Memoizes a value

useCallback
   ↓
Memoizes a function

For example:

const value = useMemo(
  () => calculateValue(data),

[data]

);

versus:

const handleClick = useCallback(
  () => handleItemClick(id),

[id]

);

useTransition

useTransition is designed for marking certain state updates as non-urgent.

Import it:

import { useTransition } from "react";

The Hook returns:

const [isPending, startTransition] = useTransition();

There are two values:

isPending

Indicates that a transition is currently in progress.

startTransition

A function used to mark state updates as non-urgent.

Why Use Transitions?

Imagine a search interface.

The user types into an input, and the application needs to display a large filtered list.

The input update is urgent.

The large result update may be less urgent.

You can tell React about this difference:

const [isPending, startTransition] = useTransition();

function handleChange(event) {
  const value = event.target.value;

  setQuery(value);

  startTransition(() => {
    setSearchResults(
      getResults(value)
    );
  });
}

The important interaction, such as typing, can remain responsive while React handles the transition update.

A Complete useTransition Example

import {
  useState,
  useTransition,
} from "react";

function SearchPage({ products }) {
  const [query, setQuery] = useState("");
  const [filteredProducts, setFilteredProducts] =
    useState(products);

  const [isPending, startTransition] =
    useTransition();

  function handleChange(event) {
    const value = event.target.value;

    setQuery(value);

    startTransition(() => {
      const results = products.filter((product) =>
        product.name
          .toLowerCase()
          .includes(value.toLowerCase())
      );

      setFilteredProducts(results);
    });
  }

  return (
    <div>
      <input
        value={query}
        onChange={handleChange}
        placeholder="Search products"
      />

      {isPending && <p>Updating results...</p>}

      <ul>
        {filteredProducts.map((product) => (
          <li key={product.id}>
            {product.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

The input update is immediate:

setQuery(value);

The expensive result update is marked as a transition:

startTransition(() => {
  setFilteredProducts(results);
});

What Is a Transition?

A transition tells React:

This update is important, but it does not need to block more urgent interactions.

Examples of updates that might be transitions include:

  • Updating a large search result
  • Switching between large views
  • Rendering a complex dashboard
  • Changing a large filtered list
  • Navigating between expensive UI states

The exact usefulness depends on the application’s workload.

isPending

The isPending value allows you to show feedback while a transition is being processed.

{isPending && <p>Loading results...</p>}

You can also disable or visually adjust controls when appropriate.

The goal is to give users feedback without making the interface feel frozen.

Important Rule for startTransition

Updates performed through startTransition are intended for non-urgent UI updates.

For example:

setInputValue(value);

startTransition(() => {
  setResults(expensiveResults);
});

The input remains an urgent update, while the result update is marked as a transition.

Transitions Are Not for Everything

Do not wrap every state update in:

startTransition(...)

For example:

startTransition(() => {
  setIsMenuOpen(true);
});

may not provide any meaningful benefit.

A transition is most useful when an update leads to expensive rendering or other work that can be deprioritized relative to urgent interactions.

useDeferredValue

useDeferredValue lets you create a deferred version of a value.

Import it:

import { useDeferredValue } from "react";

The syntax is:

const deferredValue = useDeferredValue(value);

React initially uses the current value when possible and can continue rendering with the previous value while preparing the updated value.

useDeferredValue Example

Imagine a search input:

function SearchPage() {
  const [query, setQuery] = useState("");

  const deferredQuery = useDeferredValue(query);

  return (
    <>
      <input
        value={query}
        onChange={(event) =>
          setQuery(event.target.value)
        }
      />

      <SearchResults query={deferredQuery} />
    </>
  );
}

The input uses the immediate value:

query

The expensive results component uses:

deferredQuery

This can allow the input to remain responsive while the results update is deprioritized.

useTransition vs useDeferredValue

These Hooks solve related but different problems.

useTransition

Use it when you control the state update.

startTransition(() => {
  setResults(newResults);
});

You are saying:

Treat this state update as non-urgent.

useDeferredValue

Use it when you already have a value and want to provide a deferred version of it.

const deferredQuery = useDeferredValue(query);

You are saying:

This consumer can use a deferred version of this value.

A simple mental model:

useTransition
State update → defer update

useDeferredValue
Value → defer value

A Practical Search Example

Suppose Web4Learners has a tutorial search page.

function TutorialSearch({ tutorials }) {
  const [query, setQuery] = useState("");

  const deferredQuery =
    useDeferredValue(query);

  const results = tutorials.filter((tutorial) =>
    tutorial.title
      .toLowerCase()
      .includes(deferredQuery.toLowerCase())
  );

  return (
    <section>
      <input
        value={query}
        onChange={(event) =>
          setQuery(event.target.value)
        }
        placeholder="Search React tutorials"
      />

      <p>
        Searching for: {deferredQuery}
      </p>

      <ul>
        {results.map((tutorial) => (
          <li key={tutorial.id}>
            {tutorial.title}
          </li>
        ))}
      </ul>
    </section>
  );
}

For a small list, this optimization may be unnecessary.

For an expensive UI, deferring the value can help maintain responsiveness.

useMemo and useDeferredValue Together

These tools can sometimes be used together, but they solve different problems.

For example:

const deferredQuery =
  useDeferredValue(query);

const results = useMemo(() => {
  return searchTutorials(
    tutorials,
    deferredQuery
  );
}, [tutorials, deferredQuery]);

Here:

  • useDeferredValue controls when the search value is prioritized.
  • useMemo caches the calculation result.

They should not be combined automatically. Use each only when it addresses a real performance concern.

When to Use Memoization

Memoization is an optimization, not a requirement.

Before using useMemo or useCallback, ask:

Is there actually an expensive calculation or an identity-sensitive dependency that needs optimization?

If the answer is no, normal code is often better.

Use useMemo for Expensive Calculations

For example:

const report = useMemo(() => {
  return generateLargeReport(data);
}, [data]);

This can be useful if generateLargeReport() is genuinely expensive.

Use useCallback When Function Identity Matters

For example, a memoized child receives a callback:

const handleSelect = useCallback(
  (id) => {
    setSelectedId(id);
  },
  []
);

This can help when the child’s optimization depends on the callback retaining its identity.

Use useTransition for Expensive UI Updates

If an update is not urgent and causes significant rendering work:

startTransition(() => {
  setSelectedTab("analytics");
});

may improve responsiveness.

Use useDeferredValue When a Consumer Can Lag

For example:

const deferredSearch = useDeferredValue(search);

This can be useful when a value drives expensive rendering but the input producing that value should remain responsive.

Don’t Memoize Cheap Calculations

This is unnecessary:

const fullName = useMemo(() => {
  return `${firstName} ${lastName}`;
}, [firstName, lastName]);

The calculation is trivial.

Prefer:

const fullName = `${firstName} ${lastName}`;

The simpler version is easier to read and is unlikely to have a meaningful performance disadvantage.

Common Memoization Mistakes

Mistake: Memoizing Everything

A common mistake is adding:

useMemo(...)

to every calculation.

Memoization itself has costs.

React needs to retain the cached value and compare dependencies.

If the original calculation is already cheap, memoization can make the code more complicated without providing a meaningful benefit.

Mistake: Using useCallback Everywhere

You may see:

const handleClick = useCallback(() => {
  console.log("Clicked");
}, []);

But if the function is simply used inside the same component and no identity-sensitive optimization depends on it, useCallback may not provide useful value.

Normal functions are often perfectly fine.

Mistake: Thinking useMemo Prevents Re-Renders

Consider:

const value = useMemo(
  () => calculateValue(data),

[data]

);

This does not stop the component itself from rendering.

It only memoizes the calculation.

Mistake: Incorrect Dependencies

Consider:

const result = useMemo(() => {
  return calculate(data, filter);
}, [data]);

The calculation uses both:

data
filter

but the dependency array only includes:

[data]

That is incorrect.

It should include the reactive values the calculation depends on:

const result = useMemo(() => {
  return calculate(data, filter);
}, [data, filter]);

Mistake: Using useMemo to Fix Incorrect Architecture

If a component performs unnecessary work because its state or data structure is poorly designed, adding useMemo may only hide the underlying problem.

First consider:

  • Can the calculation be moved?
  • Can unnecessary state be removed?
  • Can the component be split?
  • Is the data being recreated unnecessarily?
  • Is an Effect causing unnecessary updates?

Optimization should address the actual cause.

Mistake: Assuming Memoization Is Always Faster

Memoization has its own overhead.

For a tiny calculation:

const doubled = number * 2;

using:

useMemo(() => number * 2, [number]);

is usually unnecessary.

The memoized version adds complexity without solving an important performance problem.

Memoization and Referential Equality

JavaScript objects and functions are compared by identity.

For example:

{} === {}

is:

false

because they are different objects.

Similarly:

(() => {}) === (() => {})

is:

false

because they are different function objects.

This is one reason useMemo and useCallback can matter when working with memoized components or dependency arrays.

useMemo for Object Identity

Suppose:

const options = {
  theme,
  language,
};

A new object is created on each render.

You could use:

const options = useMemo(
  () => ({
    theme,
    language,
  }),

[theme, language]

);

Now the same object identity can be preserved while its dependencies remain unchanged.

But again, this is useful only when that identity actually matters.

useCallback for Function Identity

Similarly:

const handleSelect = useCallback(
  (id) => {
    setSelectedId(id);
  },
  []
);

can preserve the callback identity.

This is particularly useful when passing callbacks to components optimized with memo.

React Performance Should Start With Measurement

Do not optimize based only on assumptions.

If a component seems slow, first investigate:

  • Which component is rendering?
  • How often is it rendering?
  • Which calculation is expensive?
  • Is a large list being rendered?
  • Is an Effect causing repeated updates?
  • Are unnecessary state updates happening?
  • Are props changing identity?
  • Is the browser doing expensive work?

React Developer Tools can help identify rendering behavior and performance bottlenecks.

The important principle is:

Measure first, optimize the actual bottleneck.

Splitting Large Components

Sometimes the best optimization is architectural rather than a Hook.

Suppose one huge component manages:

Header
Search
Sidebar
Product List
Product Details
Reviews
Footer

A state update in the parent may cause many parts of the tree to render.

Breaking the UI into focused components can make rendering behavior easier to reason about and can provide better boundaries for optimization.

Reducing Unnecessary State

Performance problems sometimes come from storing information that can simply be calculated.

For example:

const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Instead:

const fullName = `${firstName} ${lastName}`;

Removing unnecessary state can eliminate extra updates entirely.

Memoization Is Not a Substitute for Good React Design

A well-structured component can often perform very well without extensive memoization.

Start with:

  • Clear state ownership
  • Small focused components
  • Efficient data structures
  • Stable list keys
  • Minimal Effects
  • Avoiding unnecessary state
  • Reasonable rendering boundaries

Then optimize the parts that actually need it.

A Complete Performance Example

Here is an example combining useMemo, useCallback, and memo.

import {
  memo,
  useCallback,
  useMemo,
  useState,
} from "react";

const ProductList = memo(function ProductList({
  products,
  onSelect,
}) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <button
            onClick={() => onSelect(product.id)}
          >
            {product.name}
          </button>
        </li>
      ))}
    </ul>
  );
});

function ProductPage({ products }) {
  const [query, setQuery] = useState("");
  const [selectedId, setSelectedId] =
    useState(null);

  const filteredProducts = useMemo(() => {
    return products.filter((product) =>
      product.name
        .toLowerCase()
        .includes(query.toLowerCase())
    );
  }, [products, query]);

  const handleSelect = useCallback((id) => {
    setSelectedId(id);
  }, []);

  return (
    <div>
      <input
        value={query}
        onChange={(event) =>
          setQuery(event.target.value)
        }
        placeholder="Search products"
      />

      <ProductList
        products={filteredProducts}
        onSelect={handleSelect}
      />

      {selectedId && (
        <p>Selected product: {selectedId}</p>
      )}
    </div>
  );
}

This example demonstrates:

useMemo
   ↓
Caches filtered products

useCallback
   ↓
Preserves select callback identity

memo
   ↓
Allows ProductList to skip some renders

However, this optimization should be justified by actual rendering behavior or an expensive workload.

Performance Hooks Cheat Sheet

HookMain Purpose
useMemoCache the result of a calculation
useCallbackCache a function identity
useTransitionMark state updates as non-urgent
useDeferredValueCreate a deferred version of a value

useMemo

const value = useMemo(
  () => calculateValue(data),

[data]

);

useCallback

const handleClick = useCallback(() => {
  doSomething();
}, []);

useTransition

const [isPending, startTransition] =
  useTransition();

startTransition(() => {
  setValue(newValue);
});

useDeferredValue

const deferredValue =
  useDeferredValue(value);

Choosing the Right Performance Tool

A simple decision process can help.

Is a calculation expensive?

Consider:

useMemo

Does a function need stable identity?

Consider:

useCallback

Do you control a state update that can be treated as non-urgent?

Consider:

useTransition

Do you have a value whose consumer can use an intentionally delayed version?

Consider:

useDeferredValue

Is the calculation cheap?

Do nothing.

Simple code is usually preferable.

Practice Exercise

Create a Web4Learners Tutorial Search component.

The application should:

  • Display a search input
  • Search through a large tutorial list
  • Keep the input responsive
  • Display filtered tutorials
  • Use useDeferredValue for the search query
  • Use useMemo for an expensive filtering calculation
  • Create a reusable result component
  • Use memo where appropriate
  • Use useCallback for a callback passed to the result component

A simplified starting point is:

import {
  memo,
  useCallback,
  useDeferredValue,
  useMemo,
  useState,
} from "react";

const TutorialResults = memo(function TutorialResults({
  tutorials,
  onSelect,
}) {
  return (
    <ul>
      {tutorials.map((tutorial) => (
        <li key={tutorial.id}>
          <button
            onClick={() =>
              onSelect(tutorial.id)
            }
          >
            {tutorial.title}
          </button>
        </li>
      ))}
    </ul>
  );
});

function TutorialSearch({ tutorials }) {
  const [query, setQuery] = useState("");

  const deferredQuery =
    useDeferredValue(query);

  const results = useMemo(() => {
    return tutorials.filter((tutorial) =>
      tutorial.title
        .toLowerCase()
        .includes(
          deferredQuery.toLowerCase()
        )
    );
  }, [tutorials, deferredQuery]);

  const handleSelect = useCallback((id) => {
    console.log("Selected:", id);
  }, []);

  return (
    <section>
      <input
        value={query}
        onChange={(event) =>
          setQuery(event.target.value)
        }
        placeholder="Search React tutorials"
      />

      <TutorialResults
        tutorials={results}
        onSelect={handleSelect}
      />
    </section>
  );
}

After building it, experiment by removing the performance optimizations.

Ask yourself:

  • Does the application actually become slower?
  • How large does the data need to be before the difference becomes noticeable?
  • Which component renders most often?
  • Which calculation takes the most time?

This exercise teaches an important real-world lesson: optimization should be based on actual behavior rather than assumptions.

Key Takeaways

  • React performance is about reducing unnecessary work and keeping interactions responsive.
  • useMemo caches the result of a calculation.
  • useCallback caches a function identity.
  • useTransition marks state updates as non-urgent.
  • useDeferredValue creates a deferred version of a value.
  • useMemo does not prevent the component from rendering.
  • useCallback does not make the function’s internal logic execute faster.
  • Memoization is an optimization, not something every component needs.
  • Cheap calculations usually do not need useMemo.
  • Functions usually do not need useCallback unless their identity matters.
  • useMemo and useCallback are especially useful when their stable identities interact with memoized components or dependency-based logic.
  • useTransition is useful when an update can be deprioritized relative to urgent interactions.
  • useDeferredValue is useful when a consumer can intentionally lag behind an input value.
  • useTransition and useDeferredValue improve scheduling and responsiveness rather than making the underlying computation inherently faster.
  • Correct dependency arrays are important for memoization Hooks.
  • Avoid using memoization to hide poor component architecture.
  • Removing unnecessary state and Effects can sometimes provide a bigger improvement than adding memoization.
  • Large lists and expensive calculations are common areas where performance work may become useful.
  • Component splitting can create clearer rendering boundaries.
  • Performance should be measured rather than guessed.
  • React Developer Tools can help identify rendering and performance problems.
  • The simplest implementation should generally be preferred until optimization is justified.

The most important idea to remember is:

Performance Hooks are optimization tools, not requirements. First build clear React code, identify the actual bottleneck, and then choose the Hook that addresses that specific problem.

Leave a Comment