Reducing Unnecessary Re-Renders in React Native
Course: React Native Training
Topic: Reducing Unnecessary Re-Renders
Course Link: React Native Training at JustAcademy
Demo Registration: Register for a Free Course Demo
Academy Website: JustAcademy Official Website
1. Introduction to Re-Rendering in React Native
Re-rendering is a normal part of React Native application development. When component state changes, props change, or a component consumes updated context, React may render that component again to calculate the updated user interface. Re-rendering helps applications respond to user interactions, API responses, navigation changes, and dynamic data.
However, unnecessary re-renders can introduce avoidable work. For example, a screen displaying a product list may contain a search input, product cards, a header, and a shopping cart summary. If typing in the search input causes every product card to repeat expensive calculations, the application may feel slow even though only the search results need to change.
The goal is not to eliminate all re-renders. The goal is to ensure that components update when necessary and avoid expensive work when their relevant data has not changed.
1.1 Learning Objectives
- Understand how React Native rendering and reconciliation work.
- Identify common causes of unnecessary re-renders.
- Learn how to use React.memo, useMemo, and useCallback appropriately.
- Organize state to limit the number of affected components.
- Optimize FlatList and frequently rendered list items.
- Avoid unnecessary effects, unstable props, and incorrect state updates.
- Measure rendering performance with React Developer Tools and profiling techniques.
- Build a small React Native screen using practical performance optimization techniques.
1.2 What Is a Re-Render?
A re-render occurs when React executes a component's rendering logic again to determine what its output should be. React then reconciles the new result with the previous one and updates the native interface when necessary.
A re-render does not automatically mean that every native view is destroyed and recreated. React can determine that the rendered output has not materially changed and avoid unnecessary native updates.
1.3 Example of a Re-Render
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
export default function CounterScreen() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button
title="Increase"
onPress={() => setCount(count + 1)}
/>
</View>
);
}
When the user presses the button, the state changes and React renders the component again. This is necessary because the displayed count must be updated.
2. Understanding the React Native Rendering Process
React Native uses React to describe the user interface and coordinates with its native rendering system to display the interface on Android and iOS. Understanding the stages of rendering helps developers distinguish normal updates from avoidable work.
2.1 Triggering a Render
A render may be triggered by several events:
- State update: A component calls a state setter such as setCount or setIsVisible.
- Parent re-render: A parent renders again and its child components are evaluated as part of the process.
- Props change: A component receives different input values from its parent.
- Context update: A component consuming a context receives an updated context value.
- External store update: A subscribed component is notified that relevant external state has changed.
2.2 Render Phase and Commit Phase
| Stage |
Description |
Performance Consideration |
| Render phase |
React calls component functions and calculates the next UI description. |
Expensive calculations and large component trees can increase work. |
| Reconciliation |
React compares the new output with the previous output. |
Stable component structure and keys help React identify corresponding elements. |
| Commit phase |
React applies the necessary updates to the rendered interface. |
Unnecessary changes to displayed values or styles can create avoidable work. |
| Native presentation |
The platform displays the updated interface. |
Large view hierarchies, images, animations, and layout work can affect responsiveness. |
2.3 Re-Rendering Versus Native View Updates
These concepts are related but different. A component may render again without requiring meaningful changes to native views. Conversely, a rendering problem may be only one part of a broader performance issue involving image loading, layout, JavaScript execution, or animations.
For this reason, optimization should begin by identifying the actual bottleneck instead of assuming every re-render is harmful.
3. Common Causes of Unnecessary Re-Renders
3.1 State Stored Too High in the Component Tree
When state is placed in a top-level screen or application component, updating it may cause a large portion of the component tree to render again.
For example, if a screen contains a search input, a product list, a navigation header, and a footer, storing the search text in a component that directly renders all these sections may cause each section to be evaluated when the user types.
function ProductScreen() {
const [searchText, setSearchText] = useState('');
return (
<View>
<SearchInput
value={searchText}
onChangeText={setSearchText}
/>
<ProductList />
<ScreenFooter />
</View>
);
}
This structure may be acceptable for a small screen. For a more complex screen, move state into a smaller component when that state is not required by the entire screen.
3.2 Creating New Objects During Rendering
JavaScript creates a new object whenever an object literal is evaluated. Two objects can contain identical properties and still have different references.
const settingsA = { theme: 'light' };
const settingsB = { theme: 'light' };
console.log(settingsA === settingsB); // false
If a parent creates a new object on every render and passes it to a memoized child, the child may render again because the object reference has changed.
function ProductScreen() {
return (
<ProductCard
product={{ id: 'p1', name: 'Headphones' }}
/>
);
}
When appropriate, pass primitive props such as productId and productName, reuse an existing object, or memoize the object if stable identity is important.
3.3 Creating New Callback Functions
Functions declared inside a component are created again whenever that component renders. This is usually inexpensive, but a new callback passed to a memoized child can prevent the child from skipping a render.
function ProductScreen() {
const [count, setCount] = useState(0);
return (
<View>
<Button
title="Increase"
onPress={() => setCount(count + 1)}
/>
<ProductCard onOpen={() => console.log('Opened')} />
</View>
);
}
Do not automatically wrap every function in useCallback. Stabilize callbacks when there is a measurable reason, such as passing them to a memoized child or using them as a Hook dependency.
3.4 Updating State with Equivalent Values
Repeatedly setting state to a genuinely unchanged primitive value may allow React to skip work. However, creating a new object or array with the same contents produces a different reference and may trigger a render.
// A new array reference is created.
setProducts([...products]);
// Better when no change is required:
// Avoid calling the setter in the first place.
State setters should represent real changes. Do not update state unnecessarily merely to force a component to refresh.
3.5 Unnecessary Effects That Update State
An effect that calculates derived information and stores it in state may create an additional render that could have been avoided.
// Avoid unnecessary derived state.
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
If the full name can be calculated cheaply during rendering, calculate it directly:
const fullName = firstName + ' ' + lastName;
Effects are appropriate for synchronizing with external systems, such as subscriptions, timers, or network connections. They should not be used simply to calculate values that can be derived from current props and state.
4. Using React.memo to Reduce Child Re-Renders
React.memo is a higher-order component that can let React skip rendering a component when its props have not changed. It is most useful for components that render frequently with unchanged props and perform enough work for skipping that render to matter.
4.1 Basic Example Without React.memo
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
function Greeting({ name }) {
console.log('Greeting rendered');
return <Text>Hello, {name}!</Text>;
}
export default function App() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button
title="Increase"
onPress={() => setCount(count + 1)}
/>
<Greeting name="Alex" />
</View>
);
}
When the count changes, the parent renders again. Because Greeting is not memoized, React may execute its component function again even though the name prop remains unchanged.
4.2 Optimizing with React.memo
import React, { memo, useState } from 'react';
import { View, Text, Button } from 'react-native';
const Greeting = memo(function Greeting({ name }) {
console.log('Greeting rendered');
return <Text>Hello, {name}!</Text>;
});
export default function App() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button
title="Increase"
onPress={() => setCount(count + 1)}
/>
<Greeting name="Alex" />
</View>
);
}
Now Greeting can skip rendering when its props remain equal. React.memo is a performance optimization, not an absolute guarantee that a component will never render again.
4.3 Important Limitations of React.memo
- A component can still render when its own state changes.
- A component consuming changed context can render again.
- New object, array, or function props may defeat the optimization.
- A memoized child may still render for other React-related reasons.
- Memoization may cost more complexity than it saves for very simple components.
4.4 When to Use React.memo
| Component Type |
Recommendation |
Reason |
| Complex product card |
Consider memoizing |
It may avoid repeated rendering when product props remain unchanged. |
| Large chart or visualization wrapper |
Profile and consider memoizing |
Rendering and data preparation may be expensive. |
| Simple text label |
Usually unnecessary |
The rendering cost is often negligible. |
| Component receiving new props every time |
Fix unstable props first |
Memoization may not provide a benefit. |
5. Using useCallback for Stable Function References
The useCallback Hook caches a function reference between renders while its dependencies remain unchanged. It is useful when callback identity matters to a memoized child or another Hook.
5.1 Example Without useCallback
function ProductList({ products }) {
const [selectedId, setSelectedId] = useState(null);
const handleSelect = (id) => {
setSelectedId(id);
};
return products.map(product => (
<ProductRow
key={product.id}
product={product}
onSelect={handleSelect}
/>
));
}
Whenever ProductList renders, a new handleSelect function is created. If ProductRow is memoized, this new function reference can cause each row to render again.
5.2 Example With useCallback
import React, { useCallback, useState } from 'react';
function ProductList({ products }) {
const [selectedId, setSelectedId] = useState(null);
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
return products.map(product => (
<ProductRow
key={product.id}
product={product}
onSelect={handleSelect}
selected={product.id === selectedId}
/>
));
}
The function reference remains stable while the dependencies remain unchanged. When selectedId changes, the selected prop may change for the affected rows, and those rows need to update accordingly.
5.3 Dependency Arrays
The dependency array tells React which values determine whether the cached function can be reused.
const handleOpen = useCallback(() => {
navigation.navigate('ProductDetails', { productId });
}, [navigation, productId]);
Every reactive value used inside the callback should be included in the dependencies unless it is intentionally handled through another safe pattern. Missing dependencies can cause stale values and bugs.
5.4 When useCallback Is Useful
- Passing callbacks to memoized list rows or child components.
- Providing a stable function to a Hook dependency when necessary.
- Preventing repeated callback identity changes in a performance-sensitive component.
- Supporting optimized subscription or event-handling patterns when stable references matter.
For ordinary event handlers passed to non-memoized components, useCallback often provides little or no benefit.
6. Using useMemo for Expensive Calculations
The useMemo Hook caches the result of a calculation. It is appropriate when a calculation is expensive and its dependencies remain unchanged across multiple renders, or when stable object or array identity is important to a memoized child.
6.1 Example of an Unnecessary Calculation
function ProductList({ products, searchText }) {
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(searchText.toLowerCase())
);
return <ProductResults products={filteredProducts} />;
}
The filter runs whenever ProductList renders, even if neither products nor searchText has changed. For a small list, this may be completely acceptable. For a large list or costly calculation, consider memoization after measuring performance.
6.2 Optimized Calculation with useMemo
import React, { useMemo } from 'react';
function ProductList({ products, searchText }) {
const filteredProducts = useMemo(() => {
const normalizedSearch = searchText.trim().toLowerCase();
return products.filter(product =>
product.name.toLowerCase().includes(normalizedSearch)
);
}, [products, searchText]);
return <ProductResults products={filteredProducts} />;
}
React can reuse the filtered array while both dependencies remain unchanged. If products is recreated on every parent render, however, the dependency changes and the calculation runs again. Stable input references and immutable state updates are important.
6.3 Avoid Mutating Arrays and Objects
React performance optimizations work best when state is updated immutably. Mutating an existing array or object may prevent React from recognizing that the data changed, potentially causing stale UI.
// Incorrect: mutates the existing array.
products.sort((a, b) => a.name.localeCompare(b.name));
// Better: sort a copied array.
const sortedProducts = [...products].sort(
(a, b) => a.name.localeCompare(b.name)
);
6.4 useMemo and useCallback Compared
| Feature |
useMemo |
useCallback |
| What it caches |
The result of a calculation |
A function reference |
| Typical use |
Expensive filtering, sorting, or derived objects |
Stable callbacks passed to memoized children |
| Example |
Memoizing filteredProducts |
Memoizing handleSelect |
| Dependencies |
Values used in the calculation |
Values used in the callback |
| Main caution |
Do not memoize every calculation automatically |
Do not wrap every function automatically |
7. Keeping State Local to Reduce Rendering Work
One of the most effective optimization strategies is to place state as close as possible to the components that need it. When state changes, this reduces the number of components that may need to render again.
7.1 Example of State That Can Be Localized
function ProductScreen() {
return (
<View>
<ScreenHeader />
<ProductSearch />
<ProductList />
<ScreenFooter />
</View>
);
}
function ProductSearch() {
const [searchText, setSearchText] = useState('');
return (
<TextInput
value={searchText}
onChangeText={setSearchText}
placeholder="Search products"
/>
);
}
This example illustrates localizing state, but it does not yet connect the search text to ProductList. If the list needs the search value, pass it from the nearest shared owner or use a carefully designed state-sharing approach. Do not localize state so aggressively that the required data flow becomes incorrect.
7.2 State Placement Guidelines
- Keep form input state inside the form or input component when possible.
- Keep temporary expansion, selection, and toggle state near the UI that uses it.
- Lift state only when multiple components genuinely need to coordinate around it.
- Use shared application state for data that must be consistently accessed across screens.
- Avoid storing derived values in state when they can be calculated from existing state and props.
8. Optimizing FlatList and Large Lists
React Native's FlatList is designed to render long lists efficiently by rendering items as needed rather than mounting every item at once. However, list performance still depends on row complexity, prop stability, item keys, and data updates.
8.1 Basic FlatList Example
import React from 'react';
import { FlatList, Text, View } from 'react-native';
export default function ProductListScreen({ products }) {
return (
<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>₹{item.price}</Text>
</View>
)}
/>
);
}
8.2 Use Stable Keys
Every list item should have a stable, unique key. A database ID or persistent product ID is usually preferable to an array index, especially when items can be inserted, deleted, sorted, or reordered.
<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={renderProduct}
/>
Stable keys help React associate items with their previous instances. They do not, by themselves, prevent every unnecessary render.
8.3 Extract and Memoize List Rows
import React, { memo, useCallback } from 'react';
import { FlatList, Text, Pressable, View } from 'react-native';
const ProductRow = memo(function ProductRow({ item, onSelect }) {
return (
<Pressable onPress={() => onSelect(item.id)}>
<View>
<Text>{item.name}</Text>
<Text>₹{item.price}</Text>
</View>
</Pressable>
);
});
export default function ProductListScreen({ products, onSelect }) {
const renderProduct = useCallback(
({ item }) => (
<ProductRow item={item} onSelect={onSelect} />
),
[onSelect]
);
return (
<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={renderProduct}
/>
);
}
This pattern works best when product objects and onSelect remain stable until their underlying data changes. If every product object is recreated on each update, memoized rows may still render again.
8.4 Understanding extraData
FlatList uses shallow prop comparison to determine whether it needs to update. If row rendering depends on a value outside data, such as selectedId, that dependency may need to be supplied through extraData.
<FlatList
data={products}
extraData={selectedId}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<ProductRow
item={item}
selected={item.id === selectedId}
onSelect={handleSelect}
/>
)}
/>
Use extraData for external values that affect rendered items. Do not add it indiscriminately; it should reflect genuine dependencies of the list output.
8.5 Additional FlatList Optimization Techniques
- Keep row components small and avoid expensive work inside renderItem.
- Use stable item IDs and avoid index keys for dynamic lists.
- Memoize row components when profiling shows repeated expensive renders.
- Use getItemLayout when item dimensions are fixed and the layout can be calculated accurately.
- Adjust initialNumToRender, windowSize, and batch-rendering settings based on testing.
- Optimize image sizes and use appropriately configured image loading.
- Avoid replacing the entire data array when only one item has changed.
- Use immutable updates so React can identify changed records.
9. Avoiding Unstable Props
Memoized components compare props to determine whether they can skip rendering. A new object, array, or function can make a prop appear changed even if its contents look the same.
9.1 Unstable Object Prop
// A new object is created each time the parent renders.
<ProfileCard options={{ compact: true, showBadge: false }} />
9.2 Stable Object with useMemo
const cardOptions = useMemo(
() => ({
compact: true,
showBadge: false
}),
[]
);
return <ProfileCard options={cardOptions} />;
For static configuration, defining a constant outside the component may be even simpler. Use useMemo when the object depends on reactive values or its stable identity is important.
9.3 Prefer Minimal Props
Passing only the data a child needs can make changes more predictable. For example, instead of passing a large user object to a small greeting component, pass the user's display name if that is all the component uses.
// Larger object may change for unrelated reasons.
<Greeting user={user} />
// Minimal prop when only the name is required.
<Greeting name={user.name} />
10. Preventing Context from Re-Rendering Too Many Components
Context provides values to components without passing props through every intermediate level. However, when a context provider receives a new value, components that consume that context may render again.
10.1 Common Context Problem
function AppProvider({ children }) {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);
const value = { theme, setTheme, user, setUser };
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
The provider creates a new value object on every render. Consumers may receive a changed context value even if the underlying fields have not changed.
10.2 Improve Context Value Stability
const value = useMemo(
() => ({ theme, setTheme, user, setUser }),
[theme, user, setTheme, setUser]
);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
State setter functions returned by useState have stable identities, but including them in the dependency list is valid. Memoizing the provider value can avoid updates caused solely by a newly created object. It does not stop consumers from rendering when a context value they use actually changes.
10.3 Split Context by Responsibility
If unrelated data changes frequently, consider separating contexts by purpose. For example, theme settings, authentication information, and frequently updated shopping-cart data may have different consumer groups.
- Keep context values focused on related information.
- Avoid putting rapidly changing data into a context consumed by the entire application.
- Memoize provider values when reference stability is beneficial.
- Consider selector-based state management when only a small part of a large shared store changes frequently.
11. Avoiding Unnecessary State Updates
Every state update should represent a meaningful change in application data or UI. Repeatedly updating state with newly created objects can cause additional rendering work.
11.1 Avoid Redundant Updates
// Avoid updating state if the value is already correct.
function setVisibleIfChanged(nextVisible) {
setVisible(current => {
if (current === nextVisible) {
return current;
}
return nextVisible;
});
}
This pattern is useful when an update may be requested repeatedly and the current value can be checked. Do not introduce extra comparison logic for every trivial state setter without a real need.
11.2 Use Functional State Updates
Functional state updates are useful when the next value depends on the previous value. They also help avoid stale state captured by callbacks.
const increment = useCallback(() => {
setCount(previousCount => previousCount + 1);
}, []);
Because the callback uses the state setter's functional form rather than reading count directly, it can remain stable without depending on count.
11.3 Batch Related State Changes
Modern React supports batching of many state updates so that related changes can be processed together. Avoid designing effects that repeatedly update state in a chain when a single state transition or derived value would be clearer.
12. Component Composition and Children
Component composition can reduce unnecessary work without adding memoization everywhere. A component that manages its own state can accept stable children from its parent rather than reconstructing a large subtree within its own rendering logic.
12.1 Example of a Wrapper Component
function ExpandablePanel({ children }) {
const [expanded, setExpanded] = useState(false);
return (
<View>
<Button
title={expanded ? 'Hide' : 'Show'}
onPress={() => setExpanded(value => !value)}
/>
{expanded ? children : null}
</View>
);
}
When the parent passes stable children, updating the panel's local state can avoid requiring the parent to recreate the entire subtree. The child components may still render if their own state, props, or context changes.
13. Handling Navigation and Screen Re-Renders
React Native navigation can introduce additional rendering and lifecycle behavior. A screen may remain mounted when another screen is placed above it, and navigation state or route parameters can change independently of the screen's local state.
- Keep screen-specific temporary state inside the screen or the smallest appropriate child.
- Use navigation focus effects only when the work genuinely needs to run when a screen gains focus.
- Clean up subscriptions and listeners to prevent repeated work or memory leaks.
- Avoid triggering state updates from effects that run after every render without a clear reason.
- Memoize expensive screen sections only after measuring the relevant interaction.
- Check whether a screen needs to remain mounted or whether its state should be reset when navigating away.
13.1 Avoid Unnecessary Focus-Triggered Updates
// Avoid refetching or resetting state without a clear condition.
useEffect(() => {
setLoading(true);
loadData();
}, [routeParams]);
When data loading is genuinely dependent on route parameters, use the appropriate dependencies and handle loading, cancellation, and stale responses carefully. If the screen should refresh only on focus, use the navigation library's documented focus lifecycle tools rather than simulating focus behavior through unrelated state updates.
14. Avoiding Expensive Work During Rendering
Component render functions should remain predictable and relatively inexpensive. Heavy synchronous calculations, large transformations, or repeated parsing can block JavaScript execution and make interactions feel delayed.
14.1 Expensive Filtering and Sorting
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.price - b.price);
}, [products]);
This is useful when sorting is expensive enough to matter and products remains stable until the data changes. It does not make the initial sort faster and does not help if the products reference changes on every render.
14.2 Avoid Repeated Data Transformations
- Normalize API responses at an appropriate data boundary.
- Avoid parsing the same large JSON payload on every render.
- Reuse derived data when the inputs have not changed.
- Move unrelated calculations outside frequently rendered components when appropriate.
- Use pagination or incremental processing for very large datasets.
- Do not perform network requests or mutate external data directly during rendering.
15. Profiling and Measuring Re-Renders
Optimization should be based on evidence. A component that renders frequently may not be a problem if each render is cheap. Conversely, a component that renders only occasionally may still cause a noticeable delay if its calculations are expensive.
15.1 Use React Developer Tools
React Developer Tools provides component inspection and profiling features that help developers investigate rendering behavior. Use profiling to identify components that render frequently, consume significant time, or update in response to unrelated interactions.
15.2 Add Temporary Render Logs
const ProductCard = memo(function ProductCard({ product }) {
console.log('ProductCard rendered:', product.id);
return (
<View>
<Text>{product.name}</Text>
</View>
);
});
Render logs can reveal unexpected behavior during development, but they should not be treated as a complete performance measurement. Development tools and Strict Mode can cause additional render activity that differs from production behavior.
15.3 Measure the Actual User Interaction
- Identify a slow interaction, such as typing in a search box or scrolling a product list.
- Record the baseline behavior before optimization.
- Use profiling tools to locate the components and calculations involved.
- Change one aspect of the component tree or state structure at a time.
- Repeat the same interaction and compare the results.
- Test on representative Android and iOS devices, especially lower-powered devices.
- Verify that the optimization does not introduce stale values, incorrect UI, or harder-to-maintain code.
15.4 Development Versus Production
Development builds may include extra checks, debugging instrumentation, and Strict Mode behavior that affect how often components appear to render. Use development tools to understand the component tree, then verify real responsiveness with an appropriate release or profiling build on target devices.
16. Common Mistakes to Avoid
| Mistake |
Why It Is a Problem |
Better Approach |
| Using React.memo everywhere |
Increases complexity without necessarily improving speed. |
Memoize expensive components that repeatedly receive unchanged props. |
| Using useCallback for every function |
Adds dependency management and readability costs. |
Stabilize callbacks when reference identity matters. |
| Using useMemo for trivial expressions |
Creates unnecessary complexity for cheap calculations. |
Calculate simple values directly. |
| Passing inline objects to memoized children |
Creates a new reference on each parent render. |
Pass primitive props or memoize configuration when useful. |
| Ignoring Hook dependencies |
May produce stale values and incorrect behavior. |
Declare dependencies correctly and follow the Rules of Hooks. |
| Mutating state directly |
Can prevent React from detecting changes correctly. |
Use immutable state updates. |
| Keeping all state at the app root |
Unrelated interactions can affect large parts of the component tree. |
Place state close to the components that need it. |
| Using array indexes as dynamic keys |
Can cause incorrect item identity during insertions or reordering. |
Use stable, unique IDs. |
| Storing derived values in state |
Can create extra render cycles and synchronization problems. |
Calculate derived values during rendering or memoize expensive calculations. |
| Optimizing without measuring |
May improve the wrong part of the application. |
Profile first and verify the result afterward. |
17. Best Practices for Reducing Unnecessary Re-Renders
- Keep component rendering pure and free from side effects.
- Store temporary state as close as possible to the component that uses it.
- Keep props small and avoid passing unrelated data.
- Use React.memo for components where skipping repeated rendering is beneficial.
- Use useCallback for callbacks whose stable references matter.
- Use useMemo for expensive calculations or values that need stable identity.
- Maintain immutable state updates and stable object references where practical.
- Use stable keys and appropriate FlatList configuration for long lists.
- Split large context values when unrelated data updates affect many consumers.
- Avoid unnecessary effects that trigger additional state updates.
- Keep image loading, animations, and list virtualization in mind when investigating performance.
- Use profiling tools instead of guessing which component is slow.
- Retest after each optimization to confirm that it improves responsiveness.
18. Practical Project: Optimize a Product List Screen
Project Objective: Create a React Native product list that supports searching, selecting products, and updating a selection count while minimizing avoidable work in product rows.
18.1 Project Requirements
- Display a list of products with stable IDs.
- Provide a search input that filters products by name.
- Allow users to select or deselect products.
- Show the total number of selected products.
- Use FlatList for efficient list rendering.
- Use memoization where it provides a practical benefit.
18.2 Example Implementation
import React, {
memo,
useCallback,
useMemo,
useState
} from 'react';
import {
View,
Text,
TextInput,
FlatList,
Pressable
} from 'react-native';
const ProductRow = memo(function ProductRow({
product,
selected,
onToggle
}) {
console.log('Rendering product:', product.id);
return (
<Pressable onPress={() => onToggle(product.id)}>
<View>
<Text>{product.name}</Text>
<Text>Price: ₹{product.price}</Text>
<Text>{selected ? 'Selected' : 'Not selected'}</Text>
</View>
</Pressable>
);
});
const INITIAL_PRODUCTS = [
{ id: 'p1', name: 'Wireless Headphones', price: 1999 },
{ id: 'p2', name: 'Bluetooth Speaker', price: 1499 },
{ id: 'p3', name: 'Smart Watch', price: 2999 },
{ id: 'p4', name: 'USB-C Charger', price: 799 },
{ id: 'p5', name: 'Laptop Stand', price: 1299 }
];
export default function ProductListScreen() {
const [searchText, setSearchText] = useState('');
const [selectedIds, setSelectedIds] = useState([]);
const filteredProducts = useMemo(() => {
const query = searchText.trim().toLowerCase();
return INITIAL_PRODUCTS.filter(product =>
product.name.toLowerCase().includes(query)
);
}, [searchText]);
const handleToggle = useCallback((id) => {
setSelectedIds(previousIds => {
if (previousIds.includes(id)) {
return previousIds.filter(itemId => itemId !== id);
}
return [...previousIds, id];
});
}, []);
const renderProduct = useCallback(({ item }) => (
<ProductRow
product={item}
selected={selectedIds.includes(item.id)}
onToggle={handleToggle}
/>
), [selectedIds, handleToggle]);
return (
<View>
<Text>Product Catalog</Text>
<TextInput
value={searchText}
onChangeText={setSearchText}
placeholder="Search products"
/>
<Text>Selected: {selectedIds.length}</Text>
<FlatList
data={filteredProducts}
keyExtractor={item => item.id}
renderItem={renderProduct}
/>
</View>
);
}
18.3 How the Optimization Works
- React.memo: ProductRow can skip rendering when its product, selected state, and onToggle callback remain unchanged.
- useMemo: The filtered product list is reused when searchText remains unchanged.
- useCallback: handleToggle remains stable because it uses a functional state update and has no reactive dependencies.
- FlatList: The list renders items using a virtualized list component.
- Stable keys: Each product has a unique ID.
- Immutable updates: Selection updates create a new array instead of mutating the previous one.
Important limitation: renderProduct depends on selectedIds, so its function reference changes whenever selection changes. Each visible row receives a selected boolean, and rows whose selected status changes need to update. This is correct behavior. For a very large catalog, selection membership may be optimized with a Set or a more specialized state structure, but it should be done only after measuring performance and ensuring that the update strategy remains correct.
18.4 Suggested Improvements
- Move product data to an API or data repository when the application requires dynamic inventory.
- Add loading, empty-result, and error states.
- Test the search field with a much larger dataset.
- Use a Set-based selection representation if selection lookups become a measurable bottleneck.
- Profile rendering before and after changes.
- Test scrolling and typing on a physical Android device and an iOS device.
19. Exercises and Practice Tasks
Exercise 1: Identify Unnecessary Re-Renders
Create a screen with a counter and three child components. Add render logs to each child. Change the counter and observe which components render again.
Exercise 2: Apply React.memo
Wrap one child component in React.memo. Keep its props unchanged while updating the parent counter. Compare the render logs before and after.
Exercise 3: Stabilize a Callback
Pass a callback from a parent to a memoized child. First pass a regular inline function, then use useCallback. Observe whether the child can skip rendering when its other props remain unchanged.
Exercise 4: Optimize a List
Create a FlatList containing at least 100 products. Use stable keys, extract a row component, and measure scrolling and selection behavior.
Exercise 5: Remove Derived State
Build a form that stores first name, last name, and full name separately. Refactor it so full name is derived from the first and last names. Compare the state update flow.
Exercise 6: Investigate Context
Create a context that contains theme and user information. Update the theme and observe which components render. Experiment with separating context values and stabilizing the provider value.
Exercise 7: Profile Before and After
Choose an interaction that feels slow, record its performance, apply one optimization, and record the result again. Document whether the change produced a measurable improvement.
20. Interview Questions and Answers
Q1. What is a re-render in React Native?
A re-render is when React executes a component's rendering logic again to calculate its next UI output after a relevant state, prop, context, or subscribed data update.
Q2. Are all re-renders bad?
No. Re-renders are necessary for displaying updated information. Only avoidable or expensive rendering work that harms performance should be targeted for optimization.
Q3. What is React.memo?
React.memo wraps a component so React can skip rendering it when its props remain equal. It does not prevent renders caused by the component's own state or relevant context changes.
Q4. What is the difference between useMemo and useCallback?
useMemo caches the result of a calculation, while useCallback caches a function reference. Both are performance optimizations and should be used when there is a clear benefit.
Q5. Why can inline objects affect performance?
An object literal creates a new reference each time it is evaluated. A memoized child receiving that object may treat the prop as changed even when its contents are identical.
Q6. How does state placement affect rendering?
State placed high in the component tree can cause a larger portion of the tree to render again. Keeping state near the components that use it can reduce the amount of affected UI.
Q7. How can FlatList performance be improved?
Use stable keys, simple row components, appropriate virtualization settings, stable props, and memoized rows where profiling shows a benefit. Avoid replacing unchanged data unnecessarily.
Q8. What is the purpose of extraData in FlatList?
extraData tells FlatList that its output depends on a value outside the data prop. Updating that value can trigger the list to reconsider rendering its items.
Q9. Why should derived state often be avoided?
Derived state duplicates information already available from props or other state. Keeping it synchronized can require additional effects and renders. Calculate it directly when practical.
Q10. What is a stale closure?
A stale closure occurs when a callback uses values captured from an earlier render rather than the current values. Correct Hook dependencies or functional state updates can prevent many such problems.
Q11. Does React.memo guarantee that a component never renders again?
No. It can skip rendering when props are equal, but state updates, context changes, and other React behavior can still cause the component to render.
Q12. How should developers investigate unnecessary renders?
Use React Developer Tools and render instrumentation to identify the components involved, reproduce the slow interaction, change one thing at a time, and verify the result on representative devices.
Q13. Why should useCallback not be used everywhere?
It adds dependency management and code complexity. If a function is passed to a component that is not memoized and is not used as a Hook dependency, stabilizing it may not improve performance.
Q14. How do immutable updates help React?
Immutable updates preserve unchanged references and create new references for changed data. This makes change detection more predictable and supports shallow comparison optimizations.
Q15. What is the most important performance optimization principle?
Measure first, identify the actual bottleneck, make a targeted change, and measure again. Optimizations should improve responsiveness without compromising correctness or maintainability.
21. Quick Revision Checklist
- Understand what triggers component re-renders.
- Distinguish render work from native UI updates.
- Keep state close to the components that need it.
- Use React.memo when unchanged props allow expensive rendering to be skipped.
- Use useCallback when callback reference stability matters.
- Use useMemo for expensive calculations or useful stable values.
- Keep Hook dependencies correct.
- Avoid creating unnecessary objects and arrays during frequent renders.
- Use immutable state updates.
- Optimize FlatList row components and key handling.
- Review context provider values and consumer dependencies.
- Avoid effects that update state unnecessarily.
- Use profiling tools to find actual bottlenecks.
- Test optimized screens on representative mobile devices.
- Ensure every optimization preserves correct application behavior.
22. Official Learning Resources
23. Conclusion
Reducing unnecessary re-renders is an important part of building responsive React Native applications. The most effective approach begins with understanding what causes rendering work and measuring which parts of the application are actually slow.
Developers can improve performance by keeping state local, avoiding redundant updates, maintaining stable props, using React.memo for suitable components, applying useMemo and useCallback selectively, optimizing FlatList, and preventing unnecessary effect-driven update chains.
Memoization is not a replacement for good component design. A well-structured component tree, predictable state flow, immutable updates, and targeted profiling often provide the greatest benefits. Apply optimizations carefully, validate them on real devices, and prioritize a smooth user experience along with readable, maintainable code.
Continue Learning: Visit JustAcademy's React Native Training Course or register for a course demo to continue learning React Native development.