skip to content

In a React Native FlatList, how does numColumns lay out a grid, and why does changing it at runtime throw an error?

level: middleimportance: should knowfreq 38%

answer

  1. items grouped into row views
  2. zig-zag like flexWrap
  3. not with horizontal
  4. changing it on the fly throws
  5. change the key to remount

basics

~20 s

With numColumns above 1, a React Native FlatList groups items into row views of that many items, filling rows left to right. It cannot be combined with horizontal, and changing numColumns after mount throws; change the list's key to remount it instead.

solid answer

~40 s

`numColumns` makes `FlatList` group consecutive items into rows: the list virtualizes **rows**, each rendered as a `View` with `flexDirection: 'row'` styled by `columnWrapperStyle`, and your `renderItem` still receives single items with their real index. The layout zig-zags like `flexWrap`, items should share a height (masonry is not supported), and it only works vertically — with `horizontal` it throws `numColumns does not support horizontal.` Changing `numColumns` on a mounted list throws `Changing numColumns on the fly is not supported…`; the fix the message suggests is to change the list's `key` so React mounts a fresh list, e.g. `key={`cols-${columns}`}` when rotating a tablet. Two more traps: the last row may be partly filled, so items sized with `flex: 1` stretch there; and `scrollToIndex` counts rows, not items, when there are several columns.

code

tsx · 31 lines
tsx
import {FlatList, StyleSheet, Text, View, useWindowDimensions} from 'react-native';

type Contact = {id: string; name: string};

export function ContactGrid({contacts}: {contacts: Contact[]}) {
  const {width} = useWindowDimensions();
  const columns = width >= 768 ? 3 : 1;
  const itemWidth = (width - 16 * (columns + 1)) / columns;

  return (
    <FlatList
      key={`contacts-${columns}`} // new key = fresh list when columns change
      data={contacts}
      numColumns={columns}
      keyExtractor={c => c.id}
      columnWrapperStyle={columns > 1 ? styles.row : undefined}
      contentContainerStyle={styles.content}
      renderItem={({item}) => (
        <View style={[styles.card, {width: itemWidth}]}>
          <Text>{item.name}</Text>
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  content: {padding: 16, gap: 16},
  row: {gap: 16},
  card: {padding: 12, borderWidth: 1, borderColor: '#d4d4d8'},
});

go deeper

for a junior

Know that numColumns turns a FlatList into a vertical grid, that it cannot be horizontal, and that items should share a height.

for a middle

Explain how the list groups items into row views styled by columnWrapperStyle, why changing numColumns throws, and how a key change remounts the list.

for a senior

Handle production details: rotation-driven column counts, a stretched last row, separators between rows, and scrollToIndex counting rows in a multi-column list.

for a principal

Decide when a FlatList grid is enough and when variable-height or horizontal grids justify a different component, weighing the remount cost of changing columns.

## What numColumns does `FlatList` normally renders one item per cell. With `numColumns` greater than 1, it groups consecutive items into **rows** of that many items and virtualizes the rows instead: 1. the list tells `VirtualizedList` it has `ceil(data.length / numColumns)` cells; 2. each cell's item is an array of up to `numColumns` items; 3. the row is rendered as a `View` with `flexDirection: 'row'`, composed with your `columnWrapperStyle`; 4. inside the row, your `renderItem` is called once per item, with that item's **real index** in `data`. The docs describe the result as a zig-zag, "like a `flexWrap` layout", and add that "items should all be the same height — masonry layouts are not supported". A row is as tall as its tallest item. A contacts screen on a tablet might show contacts as a three-column grid of cards; on a phone, one column. ## The constraints the list enforces | Situation | What happens | |---|---| | `numColumns > 1` with `horizontal` | invariant: `numColumns does not support horizontal.` | | `columnWrapperStyle` with one column | invariant: `columnWrapperStyle not supported for single column lists` | | `numColumns` changes after mount | invariant: `Changing numColumns on the fly is not supported. Change the key prop on FlatList when changing the number of columns to force a fresh render of the component.` | The last one is checked when the list updates, so it appears the moment you rotate a device and recompute the column count from the window width. ## Changing the column count safely The error message gives the fix: **change the `key` prop of the `FlatList`** whenever the column count changes. A new key makes React unmount the old list and mount a new one, which rebuilds the row grouping, the measurement cache and the rendered window from scratch. - Derive the key from the column count: `` key={`contacts-${columns}`} ``. - Expect the new list to start at the top; save and restore a position yourself if it matters. - Keep the column count stable while the list is mounted for any other reason. ## Sizing items in a grid - **Item width.** Either give items `flex: 1` so they share the row, or compute a fixed width from the window width. With `flex: 1`, a partly filled last row — say 301 contacts in three columns — stretches its lone item across the full width. Fixed widths, or padding the data with invisible filler items, avoid that. - **Gaps.** `columnWrapperStyle` is the place for spacing between items in a row, for example `gap` or `justifyContent`. - **Separators.** `ItemSeparatorComponent` renders between **rows**, not between items in the same row. ## Keys and scrolling with columns - Your `keyExtractor` still receives single items. Internally, the row's key is the keys of its items joined with `:`, so per-item uniqueness still matters. - `scrollToIndex` is forwarded to the underlying list, which counts **rows** when there are several columns. To scroll to item 90 in a three-column grid, pass the row index, `Math.floor(90 / 3)`. - The index passed to `renderItem` is the item's real index, not the row's. ## Why not horizontal grids? The grouping always builds horizontal rows inside a vertically scrolling list. A horizontally scrolling grid needs a different structure, for example a horizontal list whose items are columns you build yourself. ## Common mistakes - Computing `numColumns` from `useWindowDimensions` without changing the key, and crashing on rotation. - Using `flex: 1` items and shipping a stretched last card. - Passing an item index to `scrollToIndex` in a multi-column list. - Setting `columnWrapperStyle` on a list that sometimes has one column.

  • Why does the example pass columnWrapperStyle only when there is more than one column?
    `FlatList` throws `columnWrapperStyle not supported for single column lists` when the style is set while `numColumns` is 1. On a phone the grid collapses to one column, so the style must be omitted there.
  • How do you scroll a three-column FlatList to item 90?
    With several columns the list virtualizes rows, and `scrollToIndex` counts rows, so pass `Math.floor(90 / 3)`, which is row 30. Passing 90 would target row 90, which is item 270 or an out-of-range error.

saying these in an interview costs you the question

  • numColumns works with horizontal to build a sideways grid.
  • numColumns can change freely on a mounted list, for example on rotation.
  • numColumns supports masonry layouts with items of different heights.
  • ItemSeparatorComponent renders between every item in a row.
  • scrollToIndex always counts items, whatever numColumns is.