In a React Native product FlatList where every row reads the whole cart from a store, why does adding one sneaker re-render every rendered row, and how do you narrow it?
answer
- a selector's result decides the re-render
- whole cart means a new reference each time
- select a primitive per row
- rows subscribe, the list stays still
- extraData={cart} undoes the win
basics
~20 sA store re-renders a subscriber when its selected result changes, and the whole cart is a new object after every update. Give each row a selector for a primitive about its own sneaker, such as its quantity.
solid answer
~50 sZustand, Redux Toolkit's `useSelector` and Jotai atoms all decide re-renders by comparing what a component selected before and after a change. If each `FlatList` row selects the whole `cart`, every add produces a new cart object, every rendered row sees a changed result and re-renders, and a `FlatList` keeps more rows rendered than are on screen. On a low-end Android phone that is a burst of JavaScript-thread work on every tap. The narrow version selects a primitive per row, for example the quantity for this sneaker's id; after an add, one row's number changes and only that row re-renders. Selectors that derive arrays or objects must return stable references, through memoised selectors or shallow comparison, or they re-render on every change too. And because each row now subscribes itself, the list needs no `extraData={cart}`, which would force the `FlatList` to re-render its rows again.
code
tsx · 28 linesimport { memo } from 'react';
import { Pressable, Text, View } from 'react-native';
import { create } from 'zustand';
type CartState = {
quantities: Record<string, number>;
add: (id: string) => void;
};
export const useCart = create<CartState>()((set) => ({
quantities: {},
add: (id) =>
set((s) => ({ quantities: { ...s.quantities, [id]: (s.quantities[id] ?? 0) + 1 } })),
}));
export const SneakerRow = memo(function SneakerRow({ id, name }: { id: string; name: string }) {
const quantity = useCart((s) => s.quantities[id] ?? 0); // a number: only this row reacts
const add = useCart((s) => s.add); // a stable function: never triggers a re-render
return (
<View>
<Text>{name}</Text>
<Text>In cart: {quantity}</Text>
<Pressable onPress={() => add(id)}>
<Text>Add</Text>
</Pressable>
</View>
);
});go deeper
Recall that a component using a store re-renders when what it selects changes, so select only what the component shows.
Explain reference comparison, why the whole cart or a filtered array always looks new, and why primitives do not.
Design per-row subscriptions for a large list, avoid extraData coupling, and confirm the result on a low-end Android release build.
Make selector discipline a codebase convention with shared, memoised selectors, so list performance does not depend on each author remembering it.
## How a store decides who re-renders External stores used in React Native apps, such as **Zustand**, **Redux Toolkit** with React-Redux's `useSelector`, and **Jotai**, share one mechanism under different APIs: a component names the piece of state it needs, and the store re-renders that component only when that piece changes. The comparison is by reference unless you ask for something else. So the **shape of what you select** is what decides the re-render, not which store you picked. ## Why the whole cart re-renders every row Take a sneaker catalogue in a `FlatList`. Each row shows a sneaker and an "In cart: 2" label, and reads the cart like this: select `state.cart`, then look up its own id. Cart updates are immutable, so adding one sneaker creates a new cart object. For every mounted row: 1. the selector runs and returns the new cart object; 2. the store compares it with the previous one, sees a different reference, and schedules a re-render; 3. the row renders, finds its own quantity unchanged, and produces the same output. A `FlatList` keeps a window of rows rendered beyond the visible ones, so this is dozens of rows, each doing work that changes nothing. All of it runs on the JavaScript thread, which on a low-end Android device can be enough to make the add button and any JavaScript-driven animation lag. ## Narrow the selection | Row selects | Returns after adding sneaker A | Rows that re-render | |---|---|---| | whole `cart` | a new object | every rendered row | | `cart.items` array | a new array | every rendered row | | quantity for this row's id | a number, changed only for A | row A | | whether this row's id is in the cart | a boolean, changed only if A was absent | row A at most | Rules that keep it narrow: - **select primitives** (numbers, strings, booleans) where you can; they compare by value; - when a selector must derive an array or object, such as the cart lines with prices, return a **stable reference**: a memoised selector (Redux Toolkit ships `createSelector`) or the library's shallow-comparison helper; - select **actions** separately from data, so a row that only calls `add` never re-renders for data changes; - pass rows an **id** and let them select, rather than passing them slices of the cart as props. ## Keep the list itself out of it A `FlatList` is a `PureComponent`: it re-renders its rows only when its props change shallowly. The React Native docs show `extraData` as the way to tell it that something `renderItem` depends on has changed. That is right when rows get data from the parent, but with per-row store subscriptions it is counterproductive: passing `extraData={cart}` makes the list re-render its rows on every cart change, undoing the narrow selectors. Rows that subscribe themselves update without the list knowing. ## How to confirm it - Log or count renders per row while adding one sneaker; one row should re-render, not all of them. - Test on the slowest Android device you support, in a release build; dev builds exaggerate JavaScript-thread cost. - Check the badge and the cart screen separately; they are other subscribers with their own selectors. ## What narrowing does not fix Narrow selectors cut the number of re-renders. They do not make an individual row cheap, and they do not replace list configuration such as item layout and window size, which is a separate tuning exercise. They are the part of list performance that depends on how state is read.
- A row selects the cart lines it needs as a new filtered array. Why does it still re-render on every change?A filter creates a new array each time the selector runs, and the store compares by reference, so the result always looks changed. Return a stable reference instead: memoise the selector so it returns the same array while its inputs are unchanged, or use the library's shallow comparison so equal contents count as unchanged.
- Why does the cart badge in the tab bar not need the same treatment as the rows?It already selects a primitive, the item count. After an add the count changes, so the badge should re-render, and it is one component. The cost worth attacking is many components re-rendering for no visible change, which is the list-row case.
saying these in an interview costs you the question
- Switching from Context to any store library fixes re-renders by itself
- Selecting the whole cart is fine because the row only uses one field
- A selector that filters an array returns the same result when nothing relevant changed
- Rows subscribed to a store still need extraData={cart} to update
- Wrapping each row in memo stops store-triggered re-renders