1.6 Lists
Learning Objectives
- Understand why FlatList is superior to
.map()for rendering lists - Render dynamic lists efficiently with proper keys and data extraction
- Implement in-memory CRUD operations (Create, Read, Update, Delete) using state
- Build a complete todo/task app with add, delete, and toggle functionality
In this lesson, you’ll learn how to manage lists of data, which is the foundation of almost every real mobile app, from todo lists to Instagram feeds to shopping carts.
Why FlatList Over .map()?
Section titled “Why FlatList Over .map()?”You might be thinking: “Can’t I just use .map() to render a list?” Yes, but FlatList is way better for mobile apps.
The Problem with .map()
Section titled “The Problem with .map()”When you use .map() to render a list, React Native renders every single item, even if they’re off-screen:
// ❌ This renders ALL items, even if you have 10,000<ScrollView> {todos.map((todo) => ( <Text key={todo.id}>{todo.title}</Text> ))}</ScrollView>If you have 1,000 items, React Native will:
- Create 1,000 components
- Render 1,000 views
- Keep 1,000 items in memory
This kills performance on mobile devices.
FlatList: Virtualized Rendering
Section titled “FlatList: Virtualized Rendering”FlatList 1 only renders what’s visible on screen. As you scroll, it recycles off-screen items and reuses them for new items:
// ✅ This only renders visible items (maybe 10-20 at a time)<FlatList data={todos} renderItem={({ item }) => <Text>{item.title}</Text>} keyExtractor={(item) => item.id}/>Key benefits:
- Only renders visible items
- Uses far less memory (10 items vs 1,000)
- Smoother scrolling, even with huge lists
- Less CPU usage = better battery life
Deeper Dive: FlatList Virtualization
FlatList uses a technique called windowing or virtualization. Here’s what happens:
-
FlatList calculates how many items fit on screen (based on item height) and renders those + a few extras above/below (called the “overscan”)
-
As you scroll, FlatList:
- Unmounts items that moved far off-screen
- Mounts new items that are about to appear
- Reuses component instances (recycles them)
-
Only ~10-30 components exist at any time, regardless of how many items are in
data
FlatList Component
Section titled “FlatList Component”FlatList requires three essential props:
| Prop | Type | Purpose |
|---|---|---|
data | Array<T> | The array of items to render |
renderItem | ({ item }) => JSX.Element | Function that renders each item |
keyExtractor | (item, index) => string | Function that returns unique key (ID) |
Basic Example
Section titled “Basic Example”// 1. Define your data typeinterface Item { id: string; name: string;}
// 2. Create data arrayconst DATA: Item[] = [ { id: '1', name: 'React Native' }, { id: '2', name: 'TypeScript' }, { id: '3', name: 'Expo' },];
// 3. Define how to render each itemconst renderItem = ({ item }: { item: Item }) => ( <View style={styles.item}> <Text>{item.name}</Text> </View>);
// 4. Use FlatList<FlatList data={DATA} renderItem={renderItem} keyExtractor={(item) => item.id}/>;What’s happening here:
data={DATA}→ FlatList reads your array- For each visible item, FlatList calls
renderItem({ item }) keyExtractortells React which item is which (more on this next)
Keys & Reconciliation
Section titled “Keys & Reconciliation”Keys are critical for React’s performance. When you add, remove, or reorder items, React uses keys to figure out what changed.
Bad Keys
Section titled “Bad Keys”// ❌ NEVER DO THISkeyExtractor={(item, index) => index.toString()}Why is this bad? When you delete an item, all the indices shift:
Before delete:[0] "Buy milk" → key: "0"[1] "Walk dog" → key: "1" ← Delete this[2] "Study React" → key: "2"
After delete:[0] "Buy milk" → key: "0"[1] "Study React" → key: "1" ← Same key, but different item!Deeper Dive: Why React Uses Keys for Reconciliation
When state changes, React needs to figure out what changed in the UI. This process is called reconciliation.
Without keys, React compares components by position:
Old: [<Item title="A" />, <Item title="B" />, <Item title="C" />]New: [<Item title="A" />, <Item title="C" />] ← B was deletedReact sees: position 0 and 1 still have items, but position 2 is gone. It assumes items didn’t change, only the last one was removed. This is wrong!
With keys, React compares by ID:
Old: [<Item key="1" />, <Item key="2" />, <Item key="3" />]New: [<Item key="1" />, <Item key="3" />] ← key="2" is goneReact sees: key="1" and key="3" still exist, but key="2" is missing. It correctly identifies that item 2 was deleted and reuses the existing components for items 1 and 3.
For more details, see Why does React need keys?
Good Keys
Section titled “Good Keys”// ✅ Use unique, stable IDskeyExtractor={(item) => item.id}Each item has a permanent ID that never changes, even when items are added, removed, or reordered:
Before delete:[id: "abc123"] "Buy milk"[id: "def456"] "Walk dog" ← Delete this[id: "ghi789"] "Study React"
After delete:[id: "abc123"] "Buy milk"[id: "ghi789"] "Study React"React sees id="def456" is gone and id="ghi789" still exists. It removes the correct component and keeps the right one.
Generating Unique IDs
Section titled “Generating Unique IDs”For new items, use one of these methods:
// Method 1: Date.now() (simple, good for single-user apps)const newItem = { id: Date.now().toString(), title: 'New task',};
// Method 2: uuid library (best for production apps)import { v4 as uuidv4 } from 'uuid';const newItem = { id: uuidv4(), // e.g., "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" title: 'New task',};Exercises
Section titled “Exercises”These four exercises focus on independent CRUD operations. Complete each one separately as they don’t build on each other.
- Click the external link icon (top-left of each Snack) to open in a new tab.
- Click the blue “Save” button to save to your account.
- Copy the Snack URL once completed and submit on Moodle.
Display a pre-populated list of fruits using FlatList. This exercise focuses on the Read operation so you’ll just be rendering data that already exists.
- A
Fruitinterface is provided withid,name, andemojiproperties - An array of 5 fruits is provided as
FRUITS - Create a
FlatListcomponent with:data={FRUITS}keyExtractorthat returnsitem.idrenderItemthat displays the emoji (large) and name
Add a random fruit to a list when a button is clicked. This exercise focuses on the Create operation so you’ll just be adding new items to the array.
- Use
useStateto manage afruitsarray (starts empty) - When the “Add Fruit to List” button is clicked:
- Pick a random fruit from
FRUIT_OPTIONS - Create a unique ID using
Date.now().toString() - Add it to the fruits array
- Pick a random fruit from
- Render fruits in a FlatList below the button
- Show an empty state message when the list is empty
Toggle a checkbox to select/deselect fruits and update the backing array. This exercise focuses on the Update operation so you’ll just be modifying existing items in the array.
- A pre-populated array of fruits is provided with a
selected: booleanproperty - Each item in the FlatList should have a checkbox
- When a checkbox is clicked:
- Use
.map()to find the matching fruit by ID - Toggle its
selectedproperty - Call
setFruitswith the updated array
- Use
- Display the backing array (as JSON) below the list so you can verify the state is updating, not just the UI
Delete a fruit from the list when a button is clicked. This exercise focuses on the Delete operation so you’ll just be removing items from the array.
- A pre-populated array of 5 fruits is provided
- Each item in the FlatList should have a “Delete” button
- When a delete button is clicked:
- Use
.filter()to remove the fruit with matching ID - Call
setFruitswith the filtered array
- Use
- Display the backing array (as JSON) below the list so you can verify the deletion happened in the actual state
Hints
-
The
FlatListcomponent needs three essential props. Look at the basic example earlier in the lesson. What three props does everyFlatListneed? -
The
keyExtractorfunction should return a unique identifier for each fruit. Since each fruit has anidproperty, use that. The function receives anitemparameter. -
The
renderItemfunction receives{ item }as a parameter and should return JSX that displays the fruit. You can accessitem.emojianditem.nameto show the data. -
Make the emoji large (font size 40-48) and add some spacing between items. Center the content for a clean look.
-
Use
useStateto manage yourfruitsarray. Initialize it as an empty array with the correct TypeScript type:useState<Fruit[]>([]). -
When the button is clicked, you need to:
- Pick a random fruit from
FRUIT_OPTIONSusingMath.random()andMath.floor() - Create a new object with a unique
id(useDate.now().toString()) - Spread the properties from the random fruit into your new object
- Add the new fruit to your state array
- Pick a random fruit from
-
Remember, you must create a new array when updating state. Use the spread operator:
[...fruits, newFruit]adds to the end. -
Use a conditional to check
fruits.length === 0and show a message. When the list isn’t empty, render the FlatList.
-
Create a function that takes an
idparameter. Inside this function, you’ll need to find the matching fruit and flip itsselectedproperty. -
The
.map()method is perfect for updating items in an array. For each fruit, check if itsidmatches the one you’re looking for. If it does, return a new object withselectedflipped. If it doesn’t match, return the fruit unchanged. -
When you find the matching fruit, create a new object by spreading the old fruit’s properties and overriding just the
selectedfield:{ ...fruit, selected: !fruit.selected }. -
Pass your toggle function to the
onValueChangeprop of the Checkbox. You’ll need to wrap it in an arrow function that passes the specific fruit’s id:() => toggleFruit(item.id). -
Consider styling the fruit name differently when it’s selected (like a different color or strikethrough text) to make the state change obvious.
For the Delete exercise, you need to remove items from a list:
-
Create a function that takes an
idparameter. This function will use.filter()to create a new array without the item you want to remove. -
The
.filter()method keeps only items that pass a test. You want to keep all fruits except the one with the matching id. Think about the condition: keep items wherefruit.id !== id. -
Each list item needs a delete button. Add it alongside the fruit emoji and name. Use flexbox to position them (row layout with space-between).
-
The button’s
onPressneeds to call your delete function with the specific fruit’s id. Wrap it in an arrow function:() => deleteFruit(item.id). -
Watch the backing array display at the bottom. Each time you delete a fruit, the array should shrink and the corresponding object should disappear from the JSON.
Summary
Section titled “Summary”- FlatList uses virtualization to only render visible items, making it vastly more performant than
.map()for lists with many items. data(array),renderItem(function), andkeyExtractor(returns unique ID).- Never use array index as a key, always use stable, unique IDs so React can correctly identify which items changed.
- Create with
[...items, newItem], Read with FlatList rendering, Update with.map()to find and modify, Delete with.filter()to remove. - Always create new arrays when updating state, never mutate with
.push(),.splice(), or direct assignment. - Always handle the case when your list is empty to improve UX.