skip to content

In a React Native FlatList, why do an inline renderItem arrow and inline style objects in each row make every parent re-render expensive?

level: middleimportance: should knowfreq 52%

answer

  1. FlatList is a PureComponent
  2. new function identity fails shallow compare
  3. every mounted cell re-runs renderItem
  4. StyleSheet.create returns the same object
  5. styles and handlers defined once

basics

~20 s

FlatList is a PureComponent, so a new renderItem function on each render makes it re-render every mounted cell. Fresh style objects and arrow handlers then give each row new props, so memoized rows cannot skip their work.

solid answer

~50 s

`FlatList` extends `React.PureComponent`, so it only skips rendering when its props are shallowly equal. An inline `renderItem={({item}) => ...}` or `keyExtractor` is a new function on every parent render, so the list re-renders and calls `renderItem` for every **mounted** cell - not just the visible ones, since the default `windowSize` of 21 keeps about ten screens above and below mounted. Inside each row, `style={{ padding: 12 }}` and `onPress={() => open(item.id)}` create new objects and functions, so a memoized row sees changed props and renders anyway. The fix is stable references: `renderItem` in `useCallback` (or module scope), `keyExtractor` at module scope, styles from `StyleSheet.create` defined once, and rows that receive an id plus a stable handler. `StyleSheet.create` itself adds no magic in 0.87 - it returns the object you pass, frozen in development; the win is creating it once.

code

tsx · 32 lines
tsx
import { memo, useCallback } from 'react';
import { FlatList, Pressable, StyleSheet, Text } from 'react-native';

type Post = { id: string; author: string; body: string };
type RowProps = { post: Post; onOpen: (id: string) => void };

const PostRow = memo(function PostRow({ post, onOpen }: RowProps) {
  // Pressable is not memoized, so an arrow here costs nothing extra.
  return (
    <Pressable style={styles.row} onPress={() => onOpen(post.id)}>
      <Text style={styles.author}>{post.author}</Text>
      <Text numberOfLines={3}>{post.body}</Text>
    </Pressable>
  );
});

// Module scope: one function for the app's lifetime.
const keyExtractor = (post: Post) => post.id;

export function Timeline({ posts, onOpenPost }: { posts: Post[]; onOpenPost: (id: string) => void }) {
  // Stable as long as onOpenPost is stable in the parent.
  const renderItem = useCallback(
    ({ item }: { item: Post }) => <PostRow post={item} onOpen={onOpenPost} />,
    [onOpenPost],
  );
  return <FlatList data={posts} keyExtractor={keyExtractor} renderItem={renderItem} />;
}

const styles = StyleSheet.create({
  row: { paddingHorizontal: 16, paddingVertical: 12 },
  author: { fontWeight: '600', marginBottom: 4 },
});

go deeper

for a junior

Remember that FlatList skips re-rendering only when its props keep the same references, so inline renderItem, keyExtractor and style objects defeat that.

for a middle

Explain PureComponent's shallow compare, why the mounted window is larger than the screen, and what StyleSheet.create really returns in 0.87.

for a senior

Show you can audit a list screen for unstable props top to bottom, fix them without breaking updates (extraData), and prove the drop in row renders on a release build.

for a principal

Discuss whether to rely on conventions, lint rules or compiler-assisted memoization to keep list props stable across a large team.

## Why identity matters for a React Native list A `FlatList` renders rows through `renderItem`, a function you pass as a prop. Internally `FlatList` is a class that extends **`React.PureComponent`**: it re-renders only when a **shallow comparison** of its props finds a difference. Shallow comparison checks references, not contents, so: - a function written inline in JSX is a **new function** on every render of the parent; - an object literal such as `{ padding: 12 }` is a **new object** on every render; - two arrays or objects with the same contents are still "different" to a shallow compare. When a timeline screen re-renders for an unrelated reason - a badge count, a header animation state, a keystroke in a search box - an inline `renderItem` or inline `keyExtractor` means `FlatList` sees changed props and re-renders. ## How far the cost spreads A re-rendering `FlatList` calls `renderItem` for **every cell it currently has mounted**, and that is more than the visible rows: | Setting | Default | Effect on how many rows re-render | |---|---|---| | `initialNumToRender` | 10 | rows rendered in the first batch | | `windowSize` | 21 | about 10 viewports above, 10 below and the visible one stay mounted | After some scrolling, a long timeline can have many screens' worth of rows mounted. Each re-run of `renderItem` produces new row elements, and each row's own props are then compared. ## Where inline row props bite Inside the row, the same identity rule applies one level down. Suppose rows are wrapped in `memo`, the usual list-row optimisation: - `style={{ paddingHorizontal: 16 }}` - a new object each time, so the row's `style` prop changed. - `onPress={() => onOpen(item.id)}` passed **into the memoized row** - a new function each time. - `item={{ ...item, liked }}` - a new object each time. Any one of these makes the memoized row render again, so the memoization protects nothing. Style objects also cost something further down: React Native's props diffing can skip a prop whose reference is unchanged, but a new object has to be flattened and compared by value. ## What `StyleSheet.create` does and does not do A common belief is that `StyleSheet.create` registers styles natively and returns IDs. In React Native 0.87 it does neither: - it returns **the same object you passed**; - in development it **freezes** each style so accidental mutation throws; - it gives you type checking of style keys. Its real performance value is indirect: styles are usually declared **once at module scope**, so every render passes the **same reference**. An inline object in JSX loses that, whether or not the values are equal. ## The fix, as a checklist 1. **Stable `renderItem`**: define it with `useCallback` (the React Native docs recommend exactly this), or at module scope if it needs nothing from the component. 2. **Stable `keyExtractor`**: define it at module scope. 3. **Static styles in `StyleSheet.create`**, declared once. For a dynamic piece, combine a static style with a small computed one, and keep that computation inside the row. 4. **Pass ids and stable handlers to rows**: the row calls `onOpen(post.id)` itself, so the parent passes one stable function to all rows. 5. **Tell the list about outside state** with `extraData` when `renderItem` depends on something not in `data`, such as a selected id - otherwise the stable list correctly refuses to re-render. 6. **Measure**, on a release build, that row renders actually dropped. ## Pitfalls - Making `renderItem` stable but leaving an inline `keyExtractor`, `ListHeaderComponent` element or `contentContainerStyle` object - any changed prop re-renders the list. - A `useCallback` whose dependency is itself unstable, such as a handler recreated by the parent on every render. - Forgetting `extraData` after stabilising everything, so selection highlights stop updating. - Treating inline arrows as always bad: an arrow passed to a non-memoized native component such as `Pressable` inside the row costs very little.

  • After stabilising renderItem, selecting a post no longer highlights its row. Why, and what fixes it?
    The row's highlight depends on a selected id held outside `data`. With stable props, the PureComponent `FlatList` sees nothing changed and skips re-rendering. Pass the selected id as `extraData` so the list re-renders when it changes, and give the row an `isSelected` boolean.
  • Is StyleSheet.create faster than an inline object with the same values?
    Not by itself: in React Native 0.87 it returns the object you pass, frozen in development. The gain comes from declaring it once, so every render passes the same reference, which lets memoized rows and the props diff skip unchanged styles.

saying these in an interview costs you the question

  • StyleSheet.create sends styles to native and returns numeric IDs
  • FlatList deep-compares its props, so inline arrows are harmless
  • Only the visible rows re-render when FlatList re-renders
  • Any inline arrow anywhere in a row is a performance bug
  • A stable renderItem makes extraData unnecessary