Why lists and user input matter in React Native
Most mobile screens are built around two ideas: showing a collection of items (messages, products, tasks, contacts) and letting the user change something (search, type a message, toggle a setting, submit a form). React Native provides core components that cover these needs efficiently: list components for rendering many rows and input components for capturing user interaction.
This chapter focuses on rendering lists with FlatList (and when to use ScrollView), plus handling user input with TextInput, Pressable, Button, Switch, and touch events. You will also learn practical patterns such as filtering a list from a search field, adding items, validating input, and improving performance for long lists.
Rendering collections: choosing the right component
ScrollView vs FlatList (and why it matters)
ScrollView renders all its children at once. This is fine for short, static content (a settings page with a handful of options, a profile screen with a few sections). For long or dynamic lists, rendering everything can be slow and memory-heavy.
FlatList is optimized for long lists. It renders only what is visible on screen (plus a small buffer) and recycles rows as you scroll. In real apps, most “feed-like” screens should use FlatList.
- Use
ScrollViewfor small, mostly static content. - Use
FlatListfor long lists, feeds, and anything that can grow. - Use
SectionListwhen you need grouped sections (e.g., contacts by letter). This chapter focuses onFlatList.
FlatList fundamentals
The minimum you need: data, renderItem, keyExtractor
A FlatList needs an array (data) and a function that renders each row (renderItem). Each item must have a stable key so React Native can efficiently update rows. If your items have an id, use it.
- Listen to the audio with the screen off.
- Earn a certificate upon completion.
- Over 5000 courses for you to explore!
Download the app
import React from 'react';import { FlatList, Text, View } from 'react-native';const DATA = [ { id: 't1', title: 'Buy groceries' }, { id: 't2', title: 'Walk the dog' }, { id: 't3', title: 'Read 10 pages' },];export default function SimpleListScreen() { return ( <View style={{ flex: 1, padding: 16 }}> <FlatList data={DATA} keyExtractor={(item) => item.id} renderItem={({ item }) => ( <Text style={{ paddingVertical: 12, fontSize: 16 }}> {item.title} </Text> )} /> </View> );}renderItem receives an object with item, index, and separators. Most of the time you’ll use item and sometimes index for alternating row styles.
Adding spacing and separators
Instead of adding borders inside each row, you can use ItemSeparatorComponent to render a separator between items. This keeps row components simpler.
<FlatList data={DATA} keyExtractor={(item) => item.id} ItemSeparatorComponent={() => ( <View style={{ height: 1, backgroundColor: '#e5e5e5' }} /> )} renderItem={({ item }) => ( <View style={{ paddingVertical: 12 }}> <Text style={{ fontSize: 16 }}>{item.title}</Text> </View> )}/>For outer padding, prefer contentContainerStyle rather than wrapping the list in extra containers that might interfere with scrolling.
<FlatList contentContainerStyle={{ padding: 16 }} data={DATA} keyExtractor={(item) => item.id} renderItem={({ item }) => <Text>{item.title}</Text>}/>Header and empty states
Real lists need context: a title, a search bar, or a summary. Use ListHeaderComponent to render content above the first item, and ListEmptyComponent to show a message when the list has no items.
<FlatList data={DATA} keyExtractor={(item) => item.id} ListHeaderComponent={() => ( <Text style={{ fontSize: 20, fontWeight: '600', marginBottom: 12 }}> Tasks </Text> )} ListEmptyComponent={() => ( <Text style={{ color: '#666' }}>No tasks yet.</Text> )} renderItem={({ item }) => <Text>{item.title}</Text>}/>When you combine a header with a search input, the header becomes a natural place to put the input so it scrolls with the list (or you can keep it outside the list if you want it fixed).
Handling user input with core components
TextInput: capturing text reliably
TextInput is the core component for typing. The most common pattern is a controlled input: you pass a value and update it via onChangeText. This makes the UI always reflect your data.
import React, { useState } from 'react';import { TextInput, View, Text } from 'react-native';export default function SearchBox() { const [query, setQuery] = useState(''); return ( <View style={{ padding: 16 }}> <Text style={{ marginBottom: 8 }}>Search</Text> <TextInput value={query} onChangeText={setQuery} placeholder="Type to search" autoCapitalize="none" autoCorrect={false} style={{ borderWidth: 1, borderColor: '#ccc', paddingHorizontal: 12, paddingVertical: 10, borderRadius: 8, }} /> <Text style={{ marginTop: 8, color: '#666' }}>Query: {query}</Text> </View> );}placeholdershows hint text when empty.autoCapitalizeandautoCorrecthelp for emails/usernames/search.- Use
keyboardTypefor numeric/email inputs (e.g.,email-address,numeric). - Use
secureTextEntryfor passwords.
Submitting text: onSubmitEditing and returnKeyType
For search bars or chat inputs, you often want the keyboard “return” key to submit. Use onSubmitEditing and set returnKeyType to match the intent.
<TextInput value={query} onChangeText={setQuery} placeholder="Search" returnKeyType="search" onSubmitEditing={() => { // trigger search action console.log('Search for:', query); }} style={{ borderWidth: 1, borderColor: '#ccc', padding: 12, borderRadius: 8 }}/>Pressable and Button: responding to taps
Button is the simplest way to trigger an action, but it has limited styling. Pressable is more flexible and lets you change styles based on pressed state.
import { Pressable, Text } from 'react-native';function PrimaryButton({ title, onPress }) { return ( <Pressable onPress={onPress} style={({ pressed }) => ({ backgroundColor: pressed ? '#1d4ed8' : '#2563eb', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 10, alignItems: 'center', })} > <Text style={{ color: 'white', fontWeight: '600' }}>{title}</Text> </Pressable> );}Use onLongPress for secondary actions (e.g., open a context menu, mark as favorite) and disabled to prevent invalid submissions.
Switch: boolean input
Switch is ideal for on/off settings. It uses value and onValueChange.
import React, { useState } from 'react';import { Switch, View, Text } from 'react-native';export default function NotificationsToggle() { const [enabled, setEnabled] = useState(false); return ( <View style={{ flexDirection: 'row', alignItems: 'center', padding: 16 }}> <Text style={{ flex: 1, fontSize: 16 }}>Notifications</Text> <Switch value={enabled} onValueChange={setEnabled} /> </View> );}Practical step-by-step: build a searchable, editable list
This walkthrough combines list rendering and user input into a single screen: a simple “tasks” list where the user can add items, search/filter them, and tap to toggle completion.
Step 1: Define the data shape and initial items
Each task will have an id, a title, and a done boolean. The id must be stable and unique for FlatList keys.
const INITIAL_TASKS = [ { id: '1', title: 'Buy groceries', done: false }, { id: '2', title: 'Call the dentist', done: true }, { id: '3', title: 'Plan weekend trip', done: false },];Step 2: Create inputs for adding and searching
You’ll use two TextInput fields: one for a new task title and one for search. Keep them separate so searching doesn’t overwrite the add field.
import React, { useMemo, useState } from 'react';import { FlatList, Pressable, Text, TextInput, View } from 'react-native';const INITIAL_TASKS = [ { id: '1', title: 'Buy groceries', done: false }, { id: '2', title: 'Call the dentist', done: true }, { id: '3', title: 'Plan weekend trip', done: false },];export default function TasksScreen() { const [tasks, setTasks] = useState(INITIAL_TASKS); const [newTitle, setNewTitle] = useState(''); const [query, setQuery] = useState(''); return ( <View style={{ flex: 1, backgroundColor: 'white' }}> <View style={{ padding: 16, gap: 12 }}> <Text style={{ fontSize: 22, fontWeight: '700' }}>Tasks</Text> <TextInput value={query} onChangeText={setQuery} placeholder="Search tasks" autoCapitalize="none" autoCorrect={false} style={{ borderWidth: 1, borderColor: '#ddd', borderRadius: 10, padding: 12 }} /> <View style={{ flexDirection: 'row', gap: 10 }}> <TextInput value={newTitle} onChangeText={setNewTitle} placeholder="New task title" style={{ flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 10, padding: 12, }} /> <Pressable onPress={() => { // will implement in next step }} style={({ pressed }) => ({ paddingHorizontal: 14, justifyContent: 'center', borderRadius: 10, backgroundColor: pressed ? '#16a34a' : '#22c55e', })} > <Text style={{ color: 'white', fontWeight: '700' }}>Add</Text> </Pressable> </View> </View> {/* list will go here */} </View> );}Notice the layout uses a top container for inputs and leaves the rest of the screen for the list. This avoids putting the entire screen inside a ScrollView and lets FlatList handle scrolling.
Step 3: Add new items safely (trim, validate, clear)
When the user taps “Add”, you should validate the input. A common rule: ignore empty or whitespace-only titles. Then create a new task object, update the list, and clear the input.
function makeId() { return String(Date.now());}const addTask = () => { const title = newTitle.trim(); if (!title) return; const newTask = { id: makeId(), title, done: false }; setTasks((current) => [newTask, ...current]); setNewTitle('');};Using the functional form of the setter (setTasks((current) => ...)) helps avoid stale values when multiple updates happen quickly.
Now wire it to the button:
<Pressable onPress={addTask} ...> <Text style={{ color: 'white', fontWeight: '700' }}>Add</Text></Pressable>You can also submit from the keyboard:
<TextInput value={newTitle} onChangeText={setNewTitle} placeholder="New task title" returnKeyType="done" onSubmitEditing={addTask} style={{ flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 10, padding: 12 }}/>Step 4: Filter the list from the search query
Filtering is derived data: you don’t need to store a separate “filtered list” in state. Instead, compute it from tasks and query. For readability and efficiency, you can use useMemo.
const filteredTasks = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return tasks; return tasks.filter((t) => t.title.toLowerCase().includes(q));}, [tasks, query]);This keeps the source of truth as tasks while the UI shows filteredTasks.
Step 5: Render tasks with FlatList and a tappable row
Each row should be tappable to toggle completion. Use Pressable for the row and change styles based on done.
const toggleTask = (id) => { setTasks((current) => current.map((t) => (t.id === id ? { ...t, done: !t.done } : t)) );};const renderTask = ({ item }) => ( <Pressable onPress={() => toggleTask(item.id)} style={({ pressed }) => ({ paddingVertical: 14, paddingHorizontal: 16, backgroundColor: pressed ? '#f3f4f6' : 'white', })} > <Text style={{ fontSize: 16, textDecorationLine: item.done ? 'line-through' : 'none', color: item.done ? '#6b7280' : '#111827', }} > {item.title} </Text> </Pressable>);Now place the list below the header area:
<FlatList data={filteredTasks} keyExtractor={(item) => item.id} renderItem={renderTask} ItemSeparatorComponent={() => ( <View style={{ height: 1, backgroundColor: '#eee' }} /> )} ListEmptyComponent={() => ( <Text style={{ padding: 16, color: '#6b7280' }}> No tasks match your search. </Text> )}/>Step 6: Put it all together (complete screen code)
import React, { useMemo, useState } from 'react';import { FlatList, Pressable, Text, TextInput, View } from 'react-native';const INITIAL_TASKS = [ { id: '1', title: 'Buy groceries', done: false }, { id: '2', title: 'Call the dentist', done: true }, { id: '3', title: 'Plan weekend trip', done: false },];function makeId() { return String(Date.now());}export default function TasksScreen() { const [tasks, setTasks] = useState(INITIAL_TASKS); const [newTitle, setNewTitle] = useState(''); const [query, setQuery] = useState(''); const addTask = () => { const title = newTitle.trim(); if (!title) return; const newTask = { id: makeId(), title, done: false }; setTasks((current) => [newTask, ...current]); setNewTitle(''); }; const toggleTask = (id) => { setTasks((current) => current.map((t) => (t.id === id ? { ...t, done: !t.done } : t)) ); }; const filteredTasks = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return tasks; return tasks.filter((t) => t.title.toLowerCase().includes(q)); }, [tasks, query]); const renderTask = ({ item }) => ( <Pressable onPress={() => toggleTask(item.id)} style={({ pressed }) => ({ paddingVertical: 14, paddingHorizontal: 16, backgroundColor: pressed ? '#f3f4f6' : 'white', })} > <Text style={{ fontSize: 16, textDecorationLine: item.done ? 'line-through' : 'none', color: item.done ? '#6b7280' : '#111827', }} > {item.title} </Text> </Pressable> ); return ( <View style={{ flex: 1, backgroundColor: 'white' }}> <View style={{ padding: 16, gap: 12 }}> <Text style={{ fontSize: 22, fontWeight: '700' }}>Tasks</Text> <TextInput value={query} onChangeText={setQuery} placeholder="Search tasks" autoCapitalize="none" autoCorrect={false} style={{ borderWidth: 1, borderColor: '#ddd', borderRadius: 10, padding: 12 }} /> <View style={{ flexDirection: 'row', gap: 10 }}> <TextInput value={newTitle} onChangeText={setNewTitle} placeholder="New task title" returnKeyType="done" onSubmitEditing={addTask} style={{ flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 10, padding: 12, }} /> <Pressable onPress={addTask} style={({ pressed }) => ({ paddingHorizontal: 14, justifyContent: 'center', borderRadius: 10, backgroundColor: pressed ? '#16a34a' : '#22c55e', })} > <Text style={{ color: 'white', fontWeight: '700' }}>Add</Text> </Pressable> </View> </View> <FlatList data={filteredTasks} keyExtractor={(item) => item.id} renderItem={renderTask} ItemSeparatorComponent={() => ( <View style={{ height: 1, backgroundColor: '#eee' }} /> )} ListEmptyComponent={() => ( <Text style={{ padding: 16, color: '#6b7280' }}> No tasks match your search. </Text> )} /> </View> );}Common list patterns you will use often
Pull-to-refresh
Many apps refresh a list when the user pulls down. FlatList supports this with refreshing and onRefresh. Even if you’re not calling a server yet, you can wire the pattern.
const [refreshing, setRefreshing] = useState(false);const onRefresh = async () => { setRefreshing(true); try { // fetch new data here } finally { setRefreshing(false); }};<FlatList data={filteredTasks} keyExtractor={(item) => item.id} renderItem={renderTask} refreshing={refreshing} onRefresh={onRefresh}/>Infinite scrolling (load more)
For feeds, you can load more items when the user approaches the end. Use onEndReached and onEndReachedThreshold. Be careful to prevent duplicate loads by tracking a loading flag.
const [loadingMore, setLoadingMore] = useState(false);const loadMore = async () => { if (loadingMore) return; setLoadingMore(true); try { // fetch next page and append to tasks } finally { setLoadingMore(false); }};<FlatList data={filteredTasks} keyExtractor={(item) => item.id} renderItem={renderTask} onEndReached={loadMore} onEndReachedThreshold={0.4}/>Swipe actions and long press (core approach)
Advanced swipe gestures typically require additional libraries, but you can still provide secondary actions using onLongPress on a row. For example, long press to delete an item after confirmation (confirmation UI can be implemented later).
const removeTask = (id) => { setTasks((current) => current.filter((t) => t.id !== id));};const renderTask = ({ item }) => ( <Pressable onPress={() => toggleTask(item.id)} onLongPress={() => removeTask(item.id)} style={{ paddingVertical: 14, paddingHorizontal: 16 }} > <Text>{item.title}</Text> </Pressable>);Input handling details that prevent bugs
Trimming, normalization, and preventing empty submissions
Users often type spaces or paste text with newlines. Normalize early:
- Use
trim()before saving. - Consider collapsing repeated spaces for titles if needed.
- Disable the submit button when invalid to make the UI clearer.
const title = newTitle.trim();const canSubmit = title.length > 0;<Pressable disabled={!canSubmit} onPress={addTask} style={({ pressed }) => ({ opacity: canSubmit ? 1 : 0.5, backgroundColor: pressed ? '#16a34a' : '#22c55e', paddingHorizontal: 14, justifyContent: 'center', borderRadius: 10,})}> <Text style={{ color: 'white', fontWeight: '700' }}>Add</Text></Pressable>Keyboard behavior and layout
On smaller screens, the keyboard can cover inputs near the bottom. A common approach is to keep primary inputs near the top (as in the example) or use a keyboard-aware layout component. Even without extra components, you can reduce friction by:
- Submitting via
onSubmitEditingso the user doesn’t need to tap a button. - Using appropriate
returnKeyType(search,done,send). - Setting
blurOnSubmit(defaults vary) depending on whether you want the keyboard to close.
Performance tips for smooth lists
Stable keys and avoiding index as key
Using the array index as a key can cause visual glitches when inserting/removing items because rows may be reused incorrectly. Prefer a stable id. If you don’t have one, generate it when the item is created and keep it with the item.
Keep renderItem lightweight
Long lists can re-render frequently. Keep row rendering simple:
- Avoid creating deeply nested inline objects repeatedly if performance becomes an issue.
- Extract row UI into a separate component if it grows, and consider memoization later.
- Use
ItemSeparatorComponentrather than borders in every row when possible.
Useful FlatList props for tuning
When lists get large, these props can help:
initialNumToRender: how many items to render initially.windowSize: how many screens worth of content to keep rendered.removeClippedSubviews: can improve performance in some cases (test on target devices).
<FlatList data={filteredTasks} keyExtractor={(item) => item.id} renderItem={renderTask} initialNumToRender={10} windowSize={5} removeClippedSubviews={true}/>Putting input and lists together: mental model
When you combine inputs and lists, a reliable approach is:
- Keep one source of truth for your items (the array).
- Store user input values separately (search query, draft text).
- Derive what you display (filtered/sorted list) from the source of truth and input values.
- Use
FlatListfor rendering, with stable keys and a focusedrenderItem. - Use
Pressablefor row interactions andTextInputfor text entry, validating before you commit changes.