skip to content

In a React Native FlatList with inverted set, as in a live comment thread, how does inversion work and which callback loads older comments?

level: middleimportance: should knowfreq 38%

answer

  1. flip the container, flip each cell back
  2. data[0] sits at the bottom
  3. logical end is the visual top
  4. footer renders above the oldest rows
  5. store newest-first, never reverse per render

basics

~20 s

An inverted React Native FlatList flips its scroll container with a -1 scale transform and flips every cell back, so data[0] renders at the bottom. The logical end is the visual top, so onEndReached loads older comments.

solid answer

~40 s

`inverted` applies a negative scale transform to the list's scroll container and the same transform to every cell, header, footer and empty component, so the content reads upright but the list starts at the **bottom**. Keep the data **newest-first**: `data[0]` is the newest comment, pinned at the bottom where the list opens, and a new comment is prepended to index 0. The callbacks stay logical: `onEndReached` fires near the **visual top**, which is where older comments load, and the loading spinner goes in `ListFooterComponent`, which now renders above the oldest rows. `onStartReached` fires near the visual bottom and loads newer comments when the thread was opened mid-history. Loading history appends at the logical end, so nothing on screen moves; only a new comment arriving while the reader is scrolled up needs `maintainVisibleContentPosition`.

code

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

type Comment = { id: string; body: string };

type Props = {
  comments: Comment[]; // newest first: comments[0] is the latest
  loadingOlder: boolean;
  loadOlder: () => void; // guarded: ignores calls while a page is in flight
};

export function CommentThread({ comments, loadingOlder, loadOlder }: Props) {
  return (
    <FlatList
      inverted
      data={comments}
      keyExtractor={comment => comment.id}
      renderItem={({ item }) => <Text>{item.body}</Text>}
      onEndReached={loadOlder} // logical end = visual top
      onEndReachedThreshold={0.5}
      ListFooterComponent={loadingOlder ? <ActivityIndicator /> : null} // renders at the top
    />
  );
}

go deeper

for a junior

Recall that inverted makes the list start at the bottom, that data[0] is the newest item shown at the bottom, and that onEndReached loads older items at the top.

for a middle

Explain the double transform, map header, footer and both edge callbacks to their visual positions, and say why history loads do not jump.

for a senior

Handle a thread opened mid-history with onStartReached, anchor prepends while the reader is scrolled up, and choose between an inverted FlatList and FlashList's bottom-anchored mode.

for a principal

Pick one model for every bottom-anchored surface in the app, inverted or anchored, so scroll APIs, jump buttons and deep links into history behave the same everywhere.

## What inverted does `inverted` is a `FlatList` (and `VirtualizedList`) boolean prop that the docs describe in one line: it reverses the direction of scroll using scale transforms of `-1`. In the React Native 0.87 source the transform is applied twice: - to the **scroll container** - for a vertical list, `scaleY: -1` on iOS and `scale: -1` on Android; for a horizontal list, `scaleX: -1`; - to **every cell**, and to `ListHeaderComponent`, `ListFooterComponent` and `ListEmptyComponent`, which flips each one back so its text reads upright. The net effect is a list whose **start is at the bottom**. Nothing about the data model changes: the list still renders `data` from index 0 upward in its own coordinate space, and every prop still speaks in logical terms. Sticky headers follow along: the list passes `invertStickyHeaders` to its scroll view, defaulting to the value of `inverted`. ## Logical terms, visual positions For a vertical inverted list, this table is the whole mental model: | Logical term | Where it appears on screen | |---|---| | `data[0]` | at the bottom | | `ListHeaderComponent` | below `data[0]`, at the very bottom | | `ListFooterComponent` | above the last item, at the very top | | `onEndReached` | fires as the reader nears the visual top | | `onStartReached` | fires as the reader nears the visual bottom | | offset `0` | the bottom of the thread | ## Why comment threads and chats use it Take a live comment thread under a breaking news story, where newest comments belong at the bottom next to the composer: 1. **It opens at the newest comment.** Offset 0 is the bottom, so the list starts there without measuring the content and scrolling to the end. 2. **A new comment is a prepend.** Insert it at `data[0]` and it appears at the bottom, where a reader sitting at offset 0 sees it arrive. 3. **History is an append.** `onEndReached` fires near the visual top; the handler appends older comments to the end of `data`, and they appear above. Because they are added at the logical end, the content between offset 0 and the viewport does not move - no jump. 4. **The history spinner is the footer.** Put the "loading older comments" indicator in `ListFooterComponent`; it renders at the top, next to the oldest loaded comment. The same edge-callback rules apply as in any list: `onEndReached` fires once per content length while inside the threshold, so the history loader needs an in-flight guard and a flag for "no older comments". ## onStartReached and jumping into history When a notification opens the thread at a comment from yesterday, the app loads a window of comments around it rather than the newest ones. The reader then scrolls **down** toward newer comments, which is toward the logical start. `onStartReached` (with `onStartReachedThreshold`, measured in visible lengths like its end counterpart) is the callback for that direction. Newer comments are then prepended at index 0 - which is the content-moving case, so pair this with `maintainVisibleContentPosition`. ## Pitfalls - **Reversing the array to fit.** Storing comments oldest-first and calling `.reverse()` or `.slice().reverse()` in render costs a copy of the whole array on every render and hands the list a new array each time. Store newest-first. - **Double reversal.** Reversing data that is already newest-first *and* setting `inverted` puts the oldest comment at the bottom. - **Header and footer swapped.** A spinner placed in `ListHeaderComponent` appears at the bottom, next to the newest comment, not where history loads. - **Scroll APIs are logical.** `scrollToOffset({ offset: 0 })` goes to the newest comment at the bottom, and `scrollToEnd()` goes to the oldest loaded one. - **A new comment while reading history.** Prepending at index 0 while the reader is scrolled up shifts the content under them; `maintainVisibleContentPosition` is the anchoring fix. ## The FlashList alternative FlashList v2 still accepts `inverted` (on Android it uses a rotate transform, which puts the scrollbar on the left), but its docs present a second route: chat screens without inversion, using its `maintainVisibleContentPosition` (on by default there) with `startRenderingFromBottom`, and `onStartReached` to load older messages at the top. That keeps the data oldest-first and the coordinates unflipped, at the cost of relying on the library's anchoring.

  • Why does loading older comments into an inverted list not make the visible comments jump?
    Because older comments are appended at the logical end of `data`, and the scroll offset is measured from the logical start - the bottom. Content added beyond the viewport's far side does not change the distance from offset 0 to what is on screen, so the visible comments stay put. The jump only appears for inserts at the logical start, such as a new comment prepended while the reader is scrolled up.
  • Where should a Jump to latest button scroll an inverted FlatList?
    To offset 0, with `scrollToOffset({ offset: 0 })`, because in an inverted list the logical start is the newest comment at the bottom. `scrollToEnd()` goes the other way, to the oldest comment loaded so far at the visual top - the opposite of what the button promises.

saying these in an interview costs you the question

  • In an inverted FlatList, onStartReached is the callback that loads older comments at the top.
  • An inverted FlatList needs oldest-first data, so data[0] appears at the top.
  • The history spinner belongs in ListHeaderComponent of an inverted FlatList.
  • Newest-first comments must still be reversed before an inverted FlatList renders them.
  • inverted re-sorts the data array rather than flipping the rendered list.