Skip to main content

Core Components and State

So far our screens only display things. In this article, we make them interactive. We will meet TextInput, Pressable, Button, and FlatList, then combine them with useState to build a working to-do list.

TextInput

TextInput is the React Native equivalent of an HTML input. It is almost always used as a controlled component, exactly like on the web:

import { useState } from "react";
import { TextInput, StyleSheet } from "react-native";

function NameField() {
const [name, setName] = useState("");

return (
<TextInput
style={styles.input}
value={name}
onChangeText={setName}
placeholder="Type your name"
/>
);
}

const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: "#cbd5e1",
borderRadius: 8,
padding: 12,
},
});

Two differences from the web worth noting:

  1. The change handler is onChangeText and it receives the string directly, not an event object. No event.target.value needed.
  2. Inputs have no border by default — you style everything yourself.

Useful props you will reach for often: keyboardType (for example "email-address" or "numeric"), secureTextEntry for passwords, and autoCapitalize.

Button and Pressable

React Native ships a basic Button component:

import { Button } from "react-native";

<Button title="Save" onPress={() => console.log("saved")} />

It works, but it renders differently on iOS and Android and accepts almost no styling — you cannot even change its height or font. That is why most apps use Pressable instead, which is an unstyled touch wrapper you design yourself:

import { Pressable, Text, StyleSheet } from "react-native";

function PrimaryButton({ label, onPress }) {
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.button, pressed && styles.pressed]}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}

const styles = StyleSheet.create({
button: {
backgroundColor: "#2563eb",
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
alignItems: "center",
},
pressed: { opacity: 0.7 },
label: { color: "#ffffff", fontWeight: "bold" },
});
tip

The style prop of Pressable can be a function. It receives the press state, which lets you give instant visual feedback while the user's finger is down — like the opacity change above.

FlatList

For lists, do not map over an array inside a ScrollView. Use FlatList, which only renders the rows currently on screen and recycles them as you scroll. That difference matters a lot on a phone with hundreds of items.

import { FlatList, Text, View } from "react-native";

const FRUITS = [
{ id: "1", name: "Apple" },
{ id: "2", name: "Mango" },
{ id: "3", name: "Banana" },
];

function FruitList() {
return (
<FlatList
data={FRUITS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={{ padding: 12 }}>
<Text>{item.name}</Text>
</View>
)}
/>
);
}

The three props to remember:

  1. data — the array to render.
  2. renderItem — a function that receives an object containing item and returns the row.
  3. keyExtractor — returns a unique string per item, the same job as key in React on the web.

Building a to-do list

Let's combine everything into one screen: an input to type a task, a button to add it, a list of tasks, and tap-to-delete.

Replace your whole App.js with this:

App.js
import { useState } from "react";
import {
FlatList,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";

export default function App() {
const [task, setTask] = useState("");
const [todos, setTodos] = useState([]);

const addTodo = () => {
const trimmed = task.trim();
if (trimmed.length === 0) {
return;
}
setTodos((current) => [
...current,
{ id: Date.now().toString(), title: trimmed },
]);
setTask("");
};

const removeTodo = (id) => {
setTodos((current) => current.filter((todo) => todo.id !== id));
};

return (
<View style={styles.screen}>
<Text style={styles.heading}>My Tasks</Text>

<View style={styles.inputRow}>
<TextInput
style={styles.input}
value={task}
onChangeText={setTask}
placeholder="What needs to be done?"
onSubmitEditing={addTodo}
/>
<Pressable style={styles.addButton} onPress={addTodo}>
<Text style={styles.addButtonText}>Add</Text>
</Pressable>
</View>

<FlatList
data={todos}
keyExtractor={(item) => item.id}
ListEmptyComponent={
<Text style={styles.empty}>No tasks yet. Add one above!</Text>
}
renderItem={({ item }) => (
<Pressable
style={styles.todoItem}
onPress={() => removeTodo(item.id)}
>
<Text style={styles.todoText}>{item.title}</Text>
<Text style={styles.deleteHint}>tap to remove</Text>
</Pressable>
)}
/>
</View>
);
}

const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: "#f8fafc", paddingTop: 64 },
heading: {
fontSize: 28,
fontWeight: "bold",
marginBottom: 16,
paddingHorizontal: 20,
},
inputRow: {
flexDirection: "row",
paddingHorizontal: 20,
marginBottom: 16,
gap: 8,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: "#cbd5e1",
borderRadius: 8,
padding: 12,
backgroundColor: "#ffffff",
},
addButton: {
backgroundColor: "#2563eb",
borderRadius: 8,
paddingHorizontal: 20,
justifyContent: "center",
},
addButtonText: { color: "#ffffff", fontWeight: "bold" },
empty: { textAlign: "center", color: "#94a3b8", marginTop: 32 },
todoItem: {
backgroundColor: "#ffffff",
marginHorizontal: 20,
marginBottom: 8,
padding: 16,
borderRadius: 8,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
todoText: { fontSize: 16, color: "#0f172a" },
deleteHint: { fontSize: 11, color: "#94a3b8" },
});

How the pieces fit together:

  1. Two state variables: task holds the current input text, todos holds the array of tasks.
  2. addTodo trims the text, ignores empty submissions, appends a new object with a unique id, and clears the input. We use the updater function form of setTodos so we always work with the latest state.
  3. removeTodo filters the tapped item out of the array. State is never mutated — we always create a new array.
  4. onSubmitEditing on the input lets the keyboard's return key add the task too.
  5. ListEmptyComponent shows a friendly message before the first task exists.

Save and try it on your phone: type a task, press Add, watch the list grow, and tap an item to remove it.

Conclusion

You now know the interactive core of React Native: TextInput for typing, Pressable for touch, FlatList for efficient lists, and useState driving it all — the same state model you already know from React. In the next article, we grow beyond a single screen with React Navigation.