Course outline

React Native / Native descriptions and local interaction / Lesson 6

FlatList identity and durable selection

Keep item identity and durable drafts outside disposable row positions, and make every render dependency explicit.

About 4 min readContent reviewed
On this page

Lesson goals

  • A row position is not a record identity
  • Expose dependencies beyond the data array
  • Reconcile disappearing identifiers under a declared policy

A row position is not a record identity

Filtering, sorting, insertion, and pagination change an itemโ€™s position without changing which record it is. Store selection by a stable domain identifier rather than an array index. A label is not always a safe key because titles can change or collide. FlatList keyExtractor should return a stable unique string for sibling items. Duplicate identifiers are a data-boundary defect that should be rejected or resolved before rendering. Do not generate a new random key during each render to remove a warning; that replaces identity and can reset row state instead of preserving it.

Expose dependencies beyond the data array

FlatList uses shallow prop comparison, so immutable inputs and explicit external dependencies matter. The first module passes selected identifiers through extraData and uses them while describing each row. data is a stable synthetic array, while selection changes through a new array snapshot. extraData documents that rows depend on selection even though their item records are unchanged. Do not rely on an incidental new callback identity as the only update contract. The callback requests parent-owned selection changes; it does not mutate item objects. The native list and interactions are illustrative and remain unexecuted.

jsx Illustrative React Native module; requires the declared compatible application host.
import {useState} from 'react';
import {FlatList, Pressable, Text} from 'react-native';
const workshops = [{id: 'w-7', title: 'Maps'}, {id: 'w-9', title: 'Rivers'}];
export default function App() {
  const [selected, setSelected] = useState([]);
  return <FlatList data={workshops} extraData={selected}
    keyExtractor={item => item.id}
    renderItem={({item}) => <Pressable accessibilityRole="button"
      accessibilityState={{selected: selected.includes(item.id)}}
      onPress={() => setSelected(previous => previous.includes(item.id)
        ? previous.filter(id => id !== item.id) : [...previous, item.id])}>
      <Text>{item.title}{selected.includes(item.id) ? ' selected' : ''}</Text>
    </Pressable>} />;
}
Expected output
Initially Maps and Rivers; selecting w-7 predicts Maps selected while Rivers stays unchanged.

Selection is owned above the rows and identified by record ID. extraData makes that external rendering dependency explicit.

Reconcile disappearing identifiers under a declared policy

The second program clears selected identifiers absent from a refreshed page. That is a page-local policy, not a universal rule for cross-page selection. A multi-page bulk action may intentionally retain identifiers and resolve availability before submitting. Choose the policy before refreshing data so an old index cannot silently select a different item. The pure function preserves input ordering among retained identifiers and leaves the original selection unchanged. It assumes identifiers already passed uniqueness validation. Node execution can verify these value properties without claiming virtualized row behavior or native recycling tests.

javascript Standalone JavaScript model with synthetic local inputs.
function retainVisibleSelection(selected, rows) {
  const visible = new Set(rows.map(row => row.id));
  return selected.filter(id => visible.has(id));
}
const before = ['w-7', 'w-9'];
const after = retainVisibleSelection(before, [{id: 'w-9'}, {id: 'w-11'}]);
console.log(JSON.stringify(after));
console.log(JSON.stringify(before));
Expected output
["w-9"]
["w-7","w-9"]

Only w-9 remains under page-local selection. The original snapshot is unchanged, and no record is reinterpreted by position.

Virtualization changes row lifetime

A virtualized list may remove offscreen rows and later render them again. Durable drafts and domain selection therefore need an owner outside an individual row unless losing them is intentional. FlatList also trades memory and responsiveness against render-window behavior; do not copy universal window or batch settings without measuring the actual target. getItemLayout is useful when row dimensions are genuinely known, but inaccurate offsets can break scrolling, especially with dynamic text and separators. A large ScrollView eagerly mounting every row is a different workload, not an interchangeable fix for virtualization symptoms.

Test movement and lifetime instead of only visible labels

A useful list test selects or edits one record, reorders the data, inserts at the front, removes the selected record, and scrolls it outside the render window. Verify state follows the intended identifier and lifetime. Include empty results, a failing refresh with prior data, and a long Arabic row at large font scale. A pure selection test cannot prove key reconciliation, scroll offsets, touch behavior, or assistive focus. Keep those as target-host checks. This course does not run a renderer, scroll a device, or certify FlatList tuning; it provides explicit source contracts and local models.

Common errors and fixes

Selection jumps after sorting

Why it happens
The selected array position was stored instead of record identity.
How to fix it
Store a stable identifier and resolve it against current data.
Prevent it next time
Test insertion before the selected row.

A row ignores a changed external selection

Why it happens
The listโ€™s shallow update contract omitted the external dependency.
How to fix it
Use immutable inputs and explicit extraData where required.
Prevent it next time
Change selection while keeping item data unchanged.

An offscreen draft disappears unexpectedly

Why it happens
Durable work lived only in disposable row state.
How to fix it
Lift durable drafts to an owner keyed by record ID.
Prevent it next time
Exercise leaving and returning to the render window.

Check your understanding

1. Why is index a poor key for rows that can reorder?

Show worked solution

The index names a slot, not a domain record. After sorting, the same slot can hold another record and inherit the wrong local state. Use stable record identity and verify an edited row through movement; visible titles alone can miss state migration.

2. Should retainVisibleSelection be used for every paginated bulk selector?

Show worked solution

No. It deliberately implements page-local selection. A cross-page contract can retain nonvisible identifiers, but must resolve current ownership and availability before a consequential command. Define the scope explicitly rather than clearing or retaining hidden items by accident.

Key takeaways

  • A row position is not a record identity
  • Test movement and lifetime instead of only visible labels

Official references