Home » How to Find and Fix Unnecessary Re-Renders in React Native
Latest Article

How to Find and Fix Unnecessary Re-Renders in React Native

A search field starts lagging as a product list grows. Toggling a favorite causes unrelated cards to render again. A small update to shared state makes an entire screen feel slow.

These symptoms can point to unnecessary rendering, but render counts alone do not explain performance. A component that renders frequently may be inexpensive. A component that renders once may perform enough synchronous work to delay interaction.

Effective React Native performance optimization begins with identifying an expensive interaction, tracing what triggers it, and fixing the smallest relevant part of the application.

What Is an Unnecessary Re-Render?

During rendering, React calls components to determine what the interface should contain. Rendering and committing changes are separate phases, so calling a component again does not necessarily mean its underlying native views change.

State updates initiate rendering, and React normally continues through descendant components. A child can therefore render because its parent updated, even when the information displayed by the child remains unchanged. React’s render and commit documentation explains this distinction.

An unnecessary re-render is usually avoidable work that contributes no useful update. Whether it deserves optimization depends on its cost and frequency.

Common triggers include:

TriggerTypical examplePotential fix
State stored too highA text field updates a whole screenMove state closer to its consumers
Unstable propsA new object reaches a memoized childPass primitives or stabilize the object
Changing contextA cart update reaches account componentsSeparate unrelated context values
Effect-driven stateFiltering sets state after every input changeDerive the result during rendering
Broad list updatesEvery row receives the full selection objectPass each row its own selected value

Find the Problem Before Adding Memoization

Record one repeatable interaction

Start with a specific action: entering five characters, selecting one item, or opening a modal. Use consistent data and repeat the same action before and after changes.

In supported Hermes applications, React Native DevTools includes Components and Profiler panels. The Components settings offer “Highlight updates when components render.” This helps locate unexpected activity, but highlighting does not measure whether that activity is expensive.

Record the interaction in the Profiler and inspect which components contributed the most render time. React Native 0.83 and later also provide a Performance panel that combines JavaScript execution and React performance tracks. Consult the documentation matching the application’s version. React Native DevTools

A useful investigation asks:

  • Which component becomes expensive during the interaction?
  • Did its props, state, or consumed context change?
  • Is the work repeated within one interaction?
  • Does the visible interface need that work?

Measure a subtree with React Profiler

For targeted instrumentation, wrap a relevant subtree:

import { Profiler } from 'react';

function reportRender(id, phase, actualDuration, baseDuration) {
  console.log({
    id,
    phase,
    actualDuration,
    baseDuration,
  });
}

export function ProfiledCatalog() {
  return (
    <Profiler id="Catalog" onRender={reportRender}>
      <Catalog />
    </Profiler>
  );
}

Here, Catalog represents the application’s existing component.

actualDuration measures React rendering time for the current update. baseDuration estimates the cost of rendering the subtree without optimizations. Neither metric represents the complete time required to display a native frame.

Profiling adds overhead and is disabled in ordinary production builds by default. Use instrumentation for diagnosis, remove noisy logging, and verify the user experience separately. React Profiler reference

Account for development behavior

Strict Mode deliberately calls component render functions an extra time in development to expose impure logic. Consequently, console output can exaggerate apparent rendering frequency.

Those development checks are not evidence that users experience identical behavior in production. Disabling Strict Mode hides useful checks rather than fixing the underlying performance issue. React Strict Mode

Fix State Placement Before Optimizing Props

Consider a screen that stores an unfinished text input alongside an expensive dashboard. Every keystroke updates the screen, even when the dashboard does not use the text.

Moving draft state into the input component narrows the update:

import { useState } from 'react';
import { Button, TextInput, View } from 'react-native';

function SearchBox({ onSubmit }) {
  const [draft, setDraft] = useState('');

  return (
    <View>
      <TextInput value={draft} onChangeText={setDraft} />
      <Button
        title="Search"
        onPress={() => onSubmit(draft)}
      />
    </View>
  );
}

This pattern suits search-on-submit behavior. Live filtering still requires sharing the query with the results that depend on it.

The architectural principle is simple: keep state at the lowest level that can coordinate all its consumers. React explicitly recommends avoiding unnecessarily elevated transient state. React optimization guidance

Use memo and Stable Props Where They Help

memo can skip a parent-driven render when props remain unchanged. By default, React compares individual props with Object.is. Newly created objects, arrays, and functions do not compare equal to their previous instances.

However, memoization does not block updates from the component’s own state or consumed context. React memoreference

A problematic pattern is:

<ProductCard
  details={{ id: product.id, name: product.name }}
  onSelect={() => selectProduct(product.id)}
/>

Even if ProductCard is memoized, both props receive new references whenever this parent renders.

A narrower interface can make the optimization effective:

import { memo, useCallback } from 'react';
import { Pressable, Text } from 'react-native';

const ProductCard = memo(function ProductCard({
  id,
  name,
  onSelect,
}) {
  return (
    <Pressable onPress={() => onSelect(id)}>
      <Text>{name}</Text>
    </Pressable>
  );
});

function ProductEntry({ product, onOpen }) {
  const handleSelect = useCallback(
    (id) => onOpen(id),
    [onOpen]
  );

  return (
    <ProductCard
      id={product.id}
      name={product.name}
      onSelect={handleSelect}
    />
  );
}

handleSelect remains stable while onOpen remains stable. If onOpen changes on every parent render, this callback changes too.

The inline function inside ProductCard does not defeat that component’s memoization: it is created only when the card actually renders.

useCallback caches a function reference; it does not prevent its containing component from rendering. Dependencies must remain accurate to avoid stale values. React useCallback reference

Remove Effects That Create Redundant Updates

Derived data often does not need separate state.

This approach schedules an additional update after the query or product list changes:

const [filteredProducts, setFilteredProducts] = useState([]);

useEffect(() => {
  setFilteredProducts(
    products.filter((product) =>
      product.name.toLowerCase().includes(query.toLowerCase())
    )
  );
}, [products, query]);

Instead, calculate the result during rendering:

const normalizedQuery = query.trim().toLowerCase();

const filteredProducts = products.filter((product) =>
  product.name.toLowerCase().includes(normalizedQuery)
);

If measurement shows filtering is expensive, cache the calculation:

const filteredProducts = useMemo(() => {
  const normalizedQuery = query.trim().toLowerCase();

  return products.filter((product) =>
    product.name.toLowerCase().includes(normalizedQuery)
  );
}, [products, query]);

This removes the effect-driven update. Memoization additionally avoids recalculation when dependencies remain unchanged; it does not eliminate filtering when the query changes. You Might Not Need an Effect

Reduce Unrelated Context Updates

A context provider containing authentication, cart contents, theme, and notifications creates a broad subscription surface.

Consumers using that context update when the provider value changes, even if they only read one field. Wrapping those consumers in memo does not prevent context-driven updates.

Two changes can help:

  • Separate context values by responsibility and update frequency.
  • Stabilize provider objects and callbacks when their underlying values have not changed.

For example:

const value = useMemo(
  () => ({ user, signOut }),
[user, signOut]

); return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> );

This prevents an unrelated provider render from creating a different value object when both dependencies are unchanged. It does not suppress legitimate user or callback changes. React useContext reference

Optimize FlatList at the Row Boundary

List performance requires separating list updates from expensive row rendering.

A useful pattern passes each row the specific values it needs:

const renderItem = useCallback(
  ({ item }) => (
    <ProductRow
      id={item.id}
      name={item.name}
      selected={selectedIds.has(item.id)}
      onToggle={toggleProduct}
    />
  ),
  [selectedIds, toggleProduct]
);

<FlatList
  data={products}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  extraData={selectedIds}
/>

Assume ProductRow is memoized and toggleProduct has a stable reference.

Changing selection updates renderItem, so the list can reconsider visible items. Rows whose primitive props remain unchanged can still skip their component render.

extraData explicitly identifies state outside data that affects rendering. In this example, the changed renderItem already changes a list prop, but declaring the dependency remains useful. Selection must update immutably, such as by creating a new Set, rather than mutating the existing one. FlatList reference

For expensive lists, React Native also recommends lightweight rows and appropriately configured virtualization. getItemLayout can avoid measurement when item dimensions are known accurately. It should not be supplied with guessed heights for variable-content rows. Optimizing FlatList Configuration

Distinguish Re-Renders From Remounts

A component that loses input state or repeatedly runs mount behavior may be remounting.

Changing its key tells React to treat it as a different component. Defining a component function inside another component can also create a new component type on each render.

Keep component definitions at module scope and use stable identifiers for list keys. Random keys and keys derived from changing field values can reset state instead of preserving the existing component. Preserving and Resetting State

Verify the Fix in Realistic Conditions

Repeat the original interaction after each meaningful change. Compare rendering cost, visible responsiveness, and correctness.

React Native’s performance documentation emphasizes release-build testing because development mode adds overhead. At 60 Hz, approximately 16.67 milliseconds are available per frame, and lengthy JavaScript work can delay touch handling and updates. Native rendering and other work can also cause performance problems independently of React renders. React Native Performance Overview

If React Compiler is enabled and successfully compiling the relevant code, it can automate much of the memoization that otherwise requires manual work. Confirm the project configuration before assuming this optimization exists. React Compiler and memoization

The objective is a measurable improvement in the interaction. Start with the profiler, correct unnecessary state propagation and update chains, then apply targeted memoization where the evidence supports it.