Lesson 21 of 26
Avoiding Unnecessary Re-Renders
The re-render model is identical to React's, but a dropped frame is more visible on a phone, and an unmemoized FlatList renderItem is a common culprit.
Nothing about why a component re-renders is different in React Native —
that entire model, from earlier in your React learning, transfers exactly:
a component re-renders when its own state changes, its own props change,
its parent re-renders, or a context it consumes changes. React.memo,
useMemo, and useCallback all work identically here, with the identical
shallow-comparison caveats. What's worth a dedicated lesson isn't a new
mechanism — it's that the stakes are higher, and the concrete places it
bites are React Native-specific.
Same model, higher stakes
On the web, an unnecessary re-render that doesn't actually change the DOM is often genuinely free — a wasted function call, invisible to the user. On a phone, the same wasted re-render competing for time on the JS thread has a more visible failure mode: a dropped frame during a scroll or a gesture is immediately felt by a finger that's actively touching the screen, in a way a background React DOM re-render rarely is on a desktop page someone is just reading. The bar for "does this unnecessary re-render actually matter" is lower on a phone, not because the mechanics changed, but because the consequences of janky frames are more visceral.
This matters most specifically inside scrolling lists, which is exactly where React Native apps spend a large share of their rendering time.
The FlatList renderItem trap
FlatList calls your renderItem function to produce every visible row.
If renderItem is defined inline, inside the component that renders the
list, it's a brand-new function on every single render of that parent —
identical behavior, but a different reference every time:
function ChatScreen({ messages }) {
const [draft, setDraft] = useState("");
return (
<FlatList
data={messages}
keyExtractor={(item) => item.id}
// A new function every render of ChatScreen — including every
// keystroke that updates `draft`, which has nothing to do with the list
renderItem={({ item }) => <MessageRow message={item} />}
/>
);
}Typing into a draft input re-renders ChatScreen, which re-creates
renderItem, which — if MessageRow isn't memoized — re-renders every
currently visible row, on every keystroke, regardless of whether any
message actually changed. On a long list, that's real, avoidable work
competing with the very thing the user is trying to do (type).
The fix: the same tools, applied here
The fix is exactly what the React.memo and useCallback lessons already taught — applied to a component that happens to be a list row instead of an arbitrary child:
const MessageRow = memo(function MessageRow({ message }) {
return (
<View style={styles.row}>
<Text>{message.text}</Text>
</View>
);
});
function ChatScreen({ messages }) {
const [draft, setDraft] = useState("");
const renderItem = useCallback(
({ item }) => <MessageRow message={item} />,
[] // MessageRow and its own props don't depend on anything from this scope
);
return (
<FlatList
data={messages}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
);
}Two things had to change together, exactly as the React lessons predicted:
MessageRow needed React.memo so it can actually skip re-rendering when
its own props haven't changed, and renderItem needed useCallback so its
reference stays stable across renders of ChatScreen — memoizing only one
of the two accomplishes nothing, because a memoized row still "sees" a new
function reference on every parent render if renderItem itself isn't
stabilized.
Where this generalizes beyond FlatList
The same pattern shows up anywhere a parent re-renders frequently for reasons unrelated to a child that's expensive to render: a details panel next to a frequently-updating counter, a card grid next to a search input, a map marker layer next to live location updates. The diagnosis is always the same question from the React re-render lessons — "does this child's render actually need to happen again, or is it just along for the ride because its parent rendered" — applied to whichever component is visibly janky on a real device, ideally confirmed with a profiler rather than optimized on a hunch.
What to remember
- The re-render model is identical to React: parent renders, children follow by default; React.memo, useMemo, and useCallback work exactly the same way here.
- A dropped frame is more visually obvious on a phone the user is touching than the same wasted work often is on a desktop web page — the stakes are higher, not the mechanism.
- An inline, unmemoized
renderItemin aFlatListis re-created on every parent render, defeating any memoization on the row component itself. - Fixing it takes both halves together:
React.memoon the row component, anduseCallbackaroundrenderItem(and anything it depends on). - Confirm with a profiler before optimizing — the goal is skipping renders that actually cost something, not reflexively wrapping every row in memo.
Check yourself
4 questions · pass 3/4 to unlock Animations and the Native Driver
1.Is the re-render model (parent re-renders → children re-render by default; React.memo/useMemo/useCallback for opting out) any different in React Native compared to React DOM?
2.Why are the visual consequences of an unnecessary re-render often more noticeable in a React Native app than in a typical web page?
3.In a FlatList, what commonly causes every visible row to re-render on every scroll-driven state change, even rows whose own data didn't change?
4.What's the correct combination for actually skipping unnecessary row re-renders in a FlatList, applying what you already know from React.memo and useCallback?
4 left to answer