In a React Native FlatList, why can rows ignore a state change such as a selected contact, and what does extraData fix?
answer
- FlatList is a PureComponent
- same data reference, no re-render
- renderItem reads outside state
- extraData breaks the shallow equality
- memoized rows need their own prop
basics
~20 sFlatList is a PureComponent, so it skips re-rendering when its props are shallowly equal. If renderItem depends on state outside data, such as a selected id, pass that state as extraData so the list re-renders and rows see the change.
solid answer
~50 sReact Native's `FlatList` extends `PureComponent`: if `data`, `renderItem` and every other prop are `===` to the previous render, it does not re-render, and neither do its rows. That is exactly what happens when `renderItem` is stable (a `useCallback` whose dependencies do not include the changing value, or a module-level function) and it reads state that is not in `data`, such as `selectedId`, or when `data` was mutated in place. The docs' answer is `extraData`: 'a marker property for telling the list to re-render' — put anything `renderItem`, the header, footer or separators depend on outside `data` there, and treat it immutably. Two caveats: `extraData` makes the list re-run `renderItem`, but a row wrapped in `React.memo` still only updates if its own props change, so pass `selected={item.id === selectedId}`; and mutating the array in place is still a bug to fix, not something `extraData` should paper over.
code
tsx · 44 linesimport {memo, useCallback, useState} from 'react';
import {FlatList, Pressable, Text} from 'react-native';
type Contact = {id: string; name: string};
const ContactRow = memo(function ContactRow({
contact,
selected,
onSelect,
}: {
contact: Contact;
selected: boolean;
onSelect: (id: string) => void;
}) {
return (
<Pressable onPress={() => onSelect(contact.id)}>
<Text style={{fontWeight: selected ? '700' : '400'}}>{contact.name}</Text>
</Pressable>
);
});
export function ContactPicker({contacts}: {contacts: Contact[]}) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const renderItem = useCallback(
({item}: {item: Contact}) => (
<ContactRow
contact={item}
selected={item.id === selectedId} // the memoized row needs its own prop
onSelect={setSelectedId}
/>
),
[selectedId],
);
return (
<FlatList
data={contacts}
keyExtractor={c => c.id}
renderItem={renderItem}
extraData={selectedId} // re-render when selection changes
/>
);
}go deeper
Remember the rule from the docs: if renderItem uses anything besides data, such as a selected id, pass it as extraData so the list re-renders.
Explain the mechanism: FlatList is a PureComponent comparing props shallowly, so an unchanged data reference and a stable renderItem mean no re-render. Say what extraData changes.
Debug stale rows end to end: in-place mutation, stable callbacks reading outside state, memoized rows missing a derived prop, and object-literal extraData forcing needless renders.
Set the list convention for the codebase: immutable data updates, derived row props, and explicit extraData, so selection and editing state never depend on accidental re-renders.
## The symptom A 300-row contacts list lets the user tap a contact to select it. The tap sets `selectedId` in the screen's state, the screen re-renders, and yet the highlighted row does not change. Scrolling far away and back sometimes fixes it. ## Why it happens: FlatList is a PureComponent `FlatList` is implemented as a class extending `React.PureComponent`. A pure component re-renders only when a **shallow comparison** of its props finds a difference. The docs spell out the consequence: it "will not re-render if props remain shallow-equal", so everything `renderItem` depends on must be passed as a prop that is not `===` after updates, "otherwise your UI may not update on changes". The props a screen passes are usually `data`, `renderItem`, `keyExtractor` and a few slots. The trap appears when all of them stay identical: - **`data` is the same array reference.** Selection lives in separate state, or the array was mutated in place with `push` or by editing an item. - **`renderItem` is the same function.** It is wrapped in `useCallback` with dependencies that omit `selectedId` (perhaps reading it through a ref), or it is defined outside the component. With nothing changed, `FlatList` skips its render, the underlying `VirtualizedList` never sees new props, and no cell re-runs `renderItem`. ## What extraData does `extraData` is a prop with no behaviour of its own; it exists to change. The docs call it "a marker property for telling the list to re-render". When its value is not `===` to the previous one: 1. `FlatList`'s shallow comparison fails, so it re-renders; 2. the `VirtualizedList` underneath re-renders too, and it also compares `data` and `extraData` to reset its viewability tracking; 3. visible cells call `renderItem` again and can read the new selection. The rule of thumb from the docs: if `renderItem`, the header, the footer or any other render function depends on anything outside `data`, pass it in `extraData` and treat it immutably (new values, never mutation). ## When extraData is not enough | Situation | Does extraData fix it? | What to do | |---|---|---| | Stable `renderItem` reads `selectedId` from outside | yes | `extraData={selectedId}` | | Row component wrapped in `React.memo` receives only `item` | no | pass `selected={item.id === selectedId}` to the row | | Array mutated in place, same reference | only superficially | produce a new array instead | | `renderItem` recreated inline every render | not needed | the new function already changes the props | The `React.memo` case surprises people: `extraData` makes the list call `renderItem` again, but a memoized row compares its **own** props. If the selection is not one of them, the row still bails out. Put the derived value on the row. ## Why not just recreate renderItem every render? An inline `renderItem` changes on every screen render, so the list always re-renders and the bug disappears. That works, and on small lists it is fine. On a large list, it means every screen render re-renders the list and its visible rows even when nothing relevant changed, which is why teams stabilise `renderItem` — and then need `extraData` for the values it reads from outside `data`. ## A checklist for stale rows 1. Is `data` a new array after the change? If it was mutated, fix the update. 2. Does `renderItem` read anything that is not in `data`? Put it in `extraData`. 3. Is the row memoized? Give it the derived prop it needs. 4. Do the header, footer or separators read outside state? Same rule: `extraData`. ## Common mistakes - Passing a new object literal as `extraData` on every render, which forces a re-render every time and defeats the purpose. - Believing `extraData` deep-compares its value; it is a reference check like every other prop. - Using `extraData` to hide in-place mutation of `data`.
- In the example, renderItem already depends on selectedId; is extraData redundant?Here, yes: `useCallback` returns a new `renderItem` when `selectedId` changes, which already breaks the shallow comparison. `extraData` states the dependency explicitly, and it keeps working if someone later stabilises `renderItem` or a header or footer starts reading the selection. It costs nothing when the value is unchanged.
- Why is extraData={{selectedId}} a poor choice?The object literal is a new reference on every render, so the list's shallow comparison always fails and the list re-renders on every screen render, even when the selection did not change. Pass the primitive value itself, or a memoized object.
A FlatList is a pure component that behaves like a receptionist who re-checks the guest list only when handed a different sheet of paper. If you write a new name onto the same sheet, or change who is the VIP without handing over anything new, nothing is re-checked; extraData is the extra note you hand over to say something changed.
saying these in an interview costs you the question
- FlatList deep-compares data, so mutating an item in place updates the row.
- extraData stores data that renderItem can read as a second argument.
- extraData makes every React.memo row update even if its props are unchanged.
- Passing a fresh object literal as extraData is the recommended pattern.
- FlatList re-renders whenever its parent re-renders, like any component.