skip to content

In a React Native FlatList, what does keyExtractor do, and which key does the list use when you omit it?

level: juniorimportance: must knowfreq 72%

answer

  1. one string per item
  2. React key and cache key
  3. item.key, then item.id
  4. index as the last resort
  5. missing-keys warning

basics

~20 s

keyExtractor returns a unique, stable string for each FlatList item; the list uses it as the React key and to cache that cell's measurements. Without it, FlatList tries item.key, then item.id, then falls back to the index and warns about missing keys.

solid answer

~50 s

`keyExtractor(item, index)` returns the string that identifies an item. React Native's `FlatList` uses it twice: as the React `key` of the row's cell, and as the key under which the list caches that cell's measured layout and refs. If you omit it, the default extractor returns `item.key` if present, otherwise `item.id`, otherwise `String(index)` — and in that last case the list logs `VirtualizedList: missing keys for items…`. For a contacts list, `keyExtractor={c => String(c.id)}` is the usual answer: the custom extractor is typed to return a string, so numeric ids are converted. Index keys are wrong as soon as the data can be filtered, sorted or prepended, because a key then points at a different contact and both row state and cached measurements follow the wrong item. Keys must be unique within the list and stable across renders, never random.

code

tsx · 14 lines
tsx
import {FlatList, Text} from 'react-native';

type Contact = {contactId: number; name: string; phone: string};

export function ContactList({contacts}: {contacts: Contact[]}) {
  return (
    <FlatList
      data={contacts}
      // No `key` or `id` field, so the default would fall back to indices.
      keyExtractor={contact => String(contact.contactId)}
      renderItem={({item}) => <Text>{item.name}</Text>}
    />
  );
}

go deeper

for a junior

Know the signature and the default order: item.key, then item.id, then the index with a missing-keys warning. Return a stable string such as String(contact.id).

for a middle

Explain the two jobs of the key in FlatList: React identity for the cell and the key of the list's measurement cache. Show why index keys go wrong after filtering or prepending.

for a senior

Diagnose rows that show another item's state or jumpy offsets after a filter by checking key stability, including composite keys when ids are only unique per type or per page.

for a principal

Make stable item identity part of the data contract with the backend, so every list in the app can key by it instead of inventing keys screen by screen.

## What keyExtractor is for A `FlatList` renders a flat array (`data`) through a render function (`renderItem`). Each rendered row lives inside a cell that the list has to identify across renders, across scrolling and across data changes. `keyExtractor` supplies that identity: ```tsx keyExtractor={(item, index) => string} ``` The docs describe the returned key as being "used for caching and as the react key to track item re-ordering". In React Native's implementation, the key is used in several places: - as the **React `key`** of the cell component, so React matches the row to the right item when the array changes; - as the key of the list's **cell metrics cache** — the measured position and size of each rendered cell, which the list uses to decide what to render and where to scroll; - as the key for the list's **cell refs** and the index-to-key map it keeps for the rendered window. ## The default extractor When you do not pass `keyExtractor`, React Native uses a default that checks, in order: 1. `item.key`, if the item is an object with a non-null `key`; 2. `item.id`, if the item is an object with a non-null `id`; 3. `String(index)` otherwise. When the list falls back to the index, it logs a one-time warning: `VirtualizedList: missing keys for items, make sure to specify a key or id property on each item or provide a custom keyExtractor.` Data shaped with an `id` field therefore works without a custom extractor; data with `contactId` or `uuid` fields falls through to indices. ## What makes a good key | Choice | Verdict | Why | |---|---|---| | `String(contact.id)` from the backend | good | unique and stable for the item's lifetime | | a composite such as `` `${type}-${id}` `` | good | needed when ids are only unique per type | | array index | risky | identity shifts on filter, sort or insert | | `Math.random()` or a fresh UUID per render | wrong | every render looks like a new list | | a display name | risky | two contacts can share a name | The custom extractor's TypeScript signature returns a `string`, so convert numeric ids explicitly. ## Why index keys break a contacts list Imagine a 300-row contacts list with a search box. The user types "an" and the data shrinks to twelve contacts. With index keys, the cell that was key `"3"` (Andrea) is now key `"3"` for a different person: - **Row state follows the key, not the contact.** An expanded row, a text input or a pending image load stays attached to the position. - **Cached measurements follow the key.** The list's metrics cache still holds the old height for key `"3"`, so scroll offsets and the rendered window can be computed from the wrong row until it is measured again. - **Separators** that were highlighted for a leading item follow the wrong row. With `String(contact.id)`, each cell keeps its identity no matter where the contact moves in the array. ## With multiple columns When `numColumns` is greater than 1, `FlatList` groups items into rows, and the key of a row is the keys of its items joined with `:`. Your `keyExtractor` still receives single items, and uniqueness per item is still what matters. ## Common mistakes - Relying on the default with data that has no `key` or `id` field, and ignoring the missing-keys warning. - Returning a number from a custom extractor in TypeScript code instead of a string. - Using keys that are unique per page of results but collide once pages are merged. - Generating keys in `renderItem` instead of `keyExtractor`; the list's caches never see them.

  • Does a FlatList need keyExtractor when every item has an id field?
    Not strictly. The default extractor checks `item.key`, then `item.id`, so objects with an `id` are keyed by it. Many teams still pass an explicit `keyExtractor` so that a later rename of the field cannot silently push the list back to index keys, and so the key is visibly a string, for example `String(item.id)`.
  • Why is a key generated with Math.random() inside keyExtractor worse than an index key?
    Every render produces new keys, so every cell looks new: React remounts every visible row, row state is lost on each render, and the list's measurement cache never gets a hit. An index key is at least stable while the array does not change shape.
  • What happens to the list's cached measurements when keys shift after a filter?
    The metrics cache is keyed by the cell key, so a shifted key inherits the old item's measured size and offset. Until the new row is measured, the list can compute the rendered window and scroll targets from the wrong height, which shows up as jumps or brief gaps.

saying these in an interview costs you the question

  • FlatList ignores keys because it virtualizes rows anyway.
  • Without keyExtractor, FlatList always uses the array index.
  • Index keys are fine for a list that can be searched or sorted.
  • A random UUID per render is the safest unique key.
  • keyExtractor only matters for React, not for the list's own caching.