Rendering Lists and Handling User Input with Core Components

Capítulo 4

Estimated reading time: 14 minutes

+ Exercise

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 ScrollView for small, mostly static content.
  • Use FlatList for long lists, feeds, and anything that can grow.
  • Use SectionList when you need grouped sections (e.g., contacts by letter). This chapter focuses on FlatList.

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.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

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>  );}
  • placeholder shows hint text when empty.
  • autoCapitalize and autoCorrect help for emails/usernames/search.
  • Use keyboardType for numeric/email inputs (e.g., email-address, numeric).
  • Use secureTextEntry for 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 onSubmitEditing so 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 ItemSeparatorComponent rather 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 FlatList for rendering, with stable keys and a focused renderItem.
  • Use Pressable for row interactions and TextInput for text entry, validating before you commit changes.

Now answer the exercise about the content:

In a screen that shows a long, feed-like list of items that can grow over time, which approach best matches the recommended rendering strategy and why?

You are right! Congratulations, now go to the next page

You missed! Try again.

FlatList is optimized for long or dynamic lists by rendering only what is visible and recycling rows. ScrollView renders everything at once, which can be slow and memory-heavy for large lists.

Next chapter

Styling with StyleSheet and Flexbox Layout Fundamentals

Arrow Right Icon
Free Ebook cover React Native Basics: Components, Styling, and Navigation Concepts
33%

React Native Basics: Components, Styling, and Navigation Concepts

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.