Introduction
React Native is a powerful framework for building native iOS and Android apps from a single JavaScript codebase. In 2026, Expo simplifies the process for beginners—no need for Xcode or Android Studio to get started. This tutorial walks you through creating an interactive app with a counter, button, and todo list.
Why React Native? It delivers native performance, instant hot-reload, and a huge community. Imagine building an app like Instagram with JavaScript! You'll learn the basics: components, state, styles, and lists. At the end, you'll have a working app ready to deploy on Expo Go. Estimated time: 30 minutes. Perfect for web developers moving to mobile.
Prerequisites
- Node.js 20+ installed (check with
node -v) - An Android/iOS emulator or the Expo Go app on your smartphone
- Basic knowledge of JavaScript and React (hooks like useState)
- A free account on expo.dev
Install Expo CLI
npm install -g @expo/cli
expo --versionThis command installs Expo CLI globally, the official tool for managing React Native projects without complex native setup. Verify with expo --version. Use npm over yarn for 2026 compatibility to avoid errors.
Create and Launch the Initial Project
Now, generate a new Expo project. Expo handles bundling, dependencies, and cross-platform development. Download Expo Go on your phone for live testing.
Initialize the Project
npx create-expo-app MyFirstApp --template blank
cd MyFirstApp
npx expo startCreates an empty project named 'MyFirstApp' and launches it with expo start. Scan the QR code with Expo Go to see 'Hello World' on your device. The dev server runs on Metro bundler; Ctrl+C to stop.
Basic App.js with Text and Styles
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.title}>My First React Native App!</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});This code replaces the default content with a centered container and styled title. StyleSheet.create optimizes styles like CSS-in-JS. flex:1 fills the entire screen; test hot-reload by saving.
Add Interactivity with useState
Next, add local state with useState. It's like let in JS but reactive: changes trigger re-renders. We'll create a clickable counter—perfect for understanding hooks in mobile.
Interactive Counter
import { useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
export default function App() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<View style={styles.container}>
<Text style={styles.title}>Counter: {count}</Text>
<TouchableOpacity style={styles.button} onPress={increment}>
<Text style={styles.buttonText}>+1</Text>
</TouchableOpacity>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 32,
fontWeight: 'bold',
marginBottom: 20,
},
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
});Adds a count state and TouchableOpacity button that increments on onPress. Styles draw from Material Design for a native look. Pitfall: Don't forget to import useState and TouchableOpacity.
Todo List with FlatList
import { useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, TouchableOpacity, FlatList, TextInput } from 'react-native';
export default function App() {
const [tasks, setTasks] = useState([]);
const [input, setInput] = useState('');
const addTask = () => {
if (input.trim()) {
setTasks([...tasks, { id: Date.now().toString(), text: input }]);
setInput('');
}
};
const renderItem = ({ item }) => (
<View style={styles.taskItem}>
<Text style={styles.taskText}>{item.text}</Text>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>Todo List</Text>
<TextInput
style={styles.input}
value={input}
onChangeText={setInput}
placeholder="Add a task"
/>
<TouchableOpacity style={styles.button} onPress={addTask}>
<Text style={styles.buttonText}>Add</Text>
</TouchableOpacity>
<FlatList
data={tasks}
renderItem={renderItem}
keyExtractor={(item) => item.id}
style={styles.list}
/>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
},
input: {
borderWidth: 1,
borderColor: '#ccc',
padding: 12,
fontSize: 16,
borderRadius: 8,
marginBottom: 10,
},
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
marginBottom: 20,
},
buttonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
list: {
flex: 1,
},
taskItem: {
backgroundColor: '#f0f0f0',
padding: 15,
marginVertical: 5,
borderRadius: 8,
},
taskText: {
fontSize: 16,
},
});Integrates FlatList for performant lists (virtualized like Android's RecyclerView). TextInput captures input; keyExtractor is required for performance. Adds tasks dynamically without full re-renders. Pitfall: Always trim() input to avoid empty tasks.
Test on a Real Device
Your app is now complete! With expo start, scan the QR code in Expo Go. For builds: eas build after eas login.
Build for Production
npx expo install expo-dev-client eas-cli
expo login
eas build --platform allInstalls EAS CLI for cloud builds. eas build generates APK/IPA files without local setup. Submit to stores with eas submit. Saves hours compared to native config.
Best Practices
- Always use StyleSheet: 10x better performance than inline objects.
- Hooks first:
useStateanduseEffectfor state; avoid classes. - FlatList for lists: Native and optimized, not
mapinScrollView. - Test on real devices: Emulators hide real-world performance issues.
- TypeScript from the start:
npx create-expo-app --template blank-typescript.
Common Errors to Avoid
- Forgetting
flex:1on root container: App won't fill the screen. - No
keyExtractoron FlatList: Crashes or lags in production. - Unmanaged inputs:
onChangeTextwithoutvaluemakes uncontrolled fields. - Excessive inline styles: Slows rendering; centralize in StyleSheet.
Next Steps
Master navigation with @react-navigation. Explore animations with react-native-reanimated. For pro projects, integrate Firebase or Supabase.
Check out our Learni React Native courses: From beginner to expert in 4 weeks.