Hey developer 👋 Welcome
If you’ve been curious about building mobile apps but thought it was going to be a giant pain with Xcode and Android Studio… surprise! You don’t need any of that to get started. We’re going to use Expo, React Native, and Expo Router — and we’ll do it all in TypeScript (because typing is cool now 😎).
Let’s build a little app that shows a list of users and lets you upload a profile picture. Sounds fun? Let’s go!
Table of contents
Open Table of contents
Quick recap: What are these tools?
- React Native: Lets you build native mobile apps using React. Same syntax you already love.
- Expo: A set of tools that makes React Native super beginner-friendly. No native setup, just code and go.
- Expo Router: Like file-based routing in Next.js, but for mobile apps.
- TypeScript: Basically JavaScript with superpowers.
Setting things up
Open your terminal and run this:
npx create-expo-app@latest my-user-app --template default
cd my-user-app
This gives you:
- A working Expo project
- File-based navigation thanks to Expo Router
- TypeScript out of the box
Before we dive into the code, you’ll need to install a few dependencies. One of them is the expo-image-picker library, which helps us allow users to select an image from their device. Let’s install it by running:
npx expo install expo-image-picker
This command will install the expo-image-picker
library and ensure it’s compatible with your current version of Expo.
Boom 💥 you’re ready.
Time to build
We’re going to make a screen where you can:
- Enter a name
- Pick an image
- Add that user to a list
Let’s open app/index.tsx
and replace everything with this:
import { useState } from "react";
import {
View,
Text,
FlatList,
TextInput,
Image,
Button,
TouchableOpacity,
StyleSheet,
useColorScheme,
} from "react-native";
import * as ImagePicker from "expo-image-picker";
type User = {
id: string;
name: string;
image?: string;
};
export default function HomeScreen() {
const [users, setUsers] = useState<User[]>([]);
const [name, setName] = useState("");
const [image, setImage] = useState<string | undefined>(undefined);
const scheme = useColorScheme(); // Detect if the device is in light or dark mode
const textColor = scheme === "dark" ? "#fff" : "#333"; // Conditional text color
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
});
if (!result.canceled) {
setImage(result.assets[0].uri);
}
};
const addUser = () => {
if (!name.trim()) return;
const newUser: User = {
id: Date.now().toString(),
name,
image,
};
setUsers((prev) => [...prev, newUser]);
setName("");
setImage(undefined);
};
return (
<View style={styles.container}>
<Text style={[styles.title, { color: textColor }]}>
User Management App
</Text>
<View style={styles.section}>
<Text style={[styles.subTitle, { color: textColor }]}>Add a User</Text>
<TextInput
style={[
styles.input,
{
color: textColor,
borderColor: scheme === "dark" ? "#444" : "#ccc",
},
]}
placeholder="Enter name"
value={name}
onChangeText={setName}
placeholderTextColor={scheme === "dark" ? "#aaa" : "#666"} // Placeholder color based on the theme
/>
<Button title="Pick an Image" onPress={pickImage} />
{image && <Image source={{ uri: image }} style={styles.preview} />}
<TouchableOpacity
style={[
styles.addButton,
{ backgroundColor: scheme === "dark" ? "#007bff" : "#1e90ff" },
]}
onPress={addUser}
>
<Text style={styles.addButtonText}>Add to List</Text>
</TouchableOpacity>
</View>
<View style={styles.section}>
<Text style={[styles.subTitle, { color: textColor }]}>My Users</Text>
{users.length > 0 ? (
<FlatList
data={users}
keyExtractor={(item) => item.id}
contentContainerStyle={{ marginTop: 20 }}
renderItem={({ item }) => (
<View
style={[
styles.userItem,
{ backgroundColor: scheme === "dark" ? "#333" : "#fff" },
]}
>
{item.image && (
<Image source={{ uri: item.image }} style={styles.avatar} />
)}
<Text style={{ color: textColor }}>{item.name}</Text>
</View>
)}
/>
) : (
<Text style={styles.label}>No users have been added</Text>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, paddingTop: 50 },
title: { fontSize: 24, fontWeight: "bold", marginBottom: 20 },
subTitle: { fontSize: 18, fontWeight: "bold", marginBottom: 20 },
label: { fontSize: 18, fontStyle: "italic", color: "gray" },
section: { marginTop: 10, marginBottom: 10 },
input: {
borderWidth: 1,
padding: 10,
borderRadius: 8,
marginBottom: 10,
},
preview: { width: 100, height: 100, borderRadius: 8, marginTop: 10 },
addButton: {
padding: 12,
borderRadius: 8,
marginTop: 10,
alignItems: "center",
},
addButtonText: { color: "#fff", fontWeight: "bold" },
userItem: {
flexDirection: "row",
alignItems: "center",
gap: 10,
marginBottom: 10,
padding: 10,
borderRadius: 8,
},
avatar: { width: 40, height: 40, borderRadius: 20 },
});
Let’s explain the code
So what’s actually going on in this app? Let’s walk through it together.
-
We define a User type
type User = { id: string; name: string; image?: string; };
This is TypeScript saying: “Hey, a user should have an id, a name, and (optionally) an image.”
-
We set up our state
const [users, setUsers] = useState<User[]>([]); const [name, setName] = useState(""); const [image, setImage] = useState<string | undefined>(undefined);
We use useState to keep track of:
- A list of users
- The name the user is typing
- The image URI (if they’ve picked one)
-
We let the user pick an image
const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, quality: 1, });
This opens the phone’s image gallery and lets the user choose a photo. If they do, we save the image URI so we can show it later.
-
We add a user to the list
const newUser: User = { id: Date.now().toString(), name, image, }; setUsers((prev) => [...prev, newUser]);
When the user clicks “Add to List”, we create a new user object and add it to the list. We also reset the input and image so the form clears.
-
We render the list of users
<FlatList data={users} keyExtractor={(item) => item.id} renderItem={({ item }) => ( <View style={styles.userItem}> {item.image && ( <Image source={{ uri: item.image }} style={styles.avatar} /> )} <Text>{item.name}</Text> </View> )} />
We use FlatList to render each user in the list. If the user picked an image, we show it as a little avatar next to their name.
-
Handling Dark and Light Modes To make sure your app looks great in both light and dark modes, we use the useColorScheme hook from React Native. This hook detects whether the device is in light mode or dark mode, allowing us to dynamically adjust our styles.
const scheme = useColorScheme(); // Detect if the device is in light or dark mode
Based on the theme, we adjust the text color and the background color:
const textColor = scheme === "dark" ? "#fff" : "#333"; // Conditional text color
In the code, I used textColor to set the text color based on the theme. The placeholder text color is also adjusted:
placeholderTextColor={scheme === "dark" ? "#aaa" : "#666"} // Placeholder color based on the theme
Additionally, when rendering the list of users, we set the background color of each userItem based on the theme:
<View style={[styles.userItem, { backgroundColor: scheme === "dark" ? "#333" : "#fff" }]}>
This way, the text and backgrounds automatically adjust depending on whether the device is in light or dark mode. Your app will seamlessly match the system theme without you needing to manage it manually!
-
And we added some simple styles. The
StyleSheet.create
part at the bottom just gives everything a bit of spacing and color. Totally optional, but it makes things look nice!
This is a small app, but it shows off some powerful stuff:
- How to use state in React Native
- How to handle forms and inputs
- How to work with images
- And how to render lists of data
Pretty awesome, huh?
Try it out
Time to run your app! In your project folder, run:
npm start # or "npx expo start"
This starts the Expo development server and shows a QR code right in your terminal.
Now grab your phone and open the camera app. Point it at the QR code — and if you already have Expo Go installed, your phone will prompt you to open the app. Tap it, and boom 💥 — your app is running on your phone. No builds. No cables. No headaches. Just live mobile development — instantly.
🤔 What’s Expo Go again? Expo Go is like a mobile browser for your React Native app. It lets you preview your code in real-time without compiling anything natively, and:
- Works on both iOS and Android
- Reloads instantly when you save changes
- Feels like magic when it just works
- Just make sure your phone and computer are on the same Wi-Fi network, and you’re good to go!
What we just did
- Set up a React Native + Expo app using TypeScript
- Used Expo Router (hello navigation!)
- Let users type a name and upload an image
- Displayed the list on the screen
All that in a single file. Pretty cool, huh?
Where to Go from Here
Nice work! You just built a working mobile app using React Native, Expo, Expo Router, and TypeScript — no messy native setup, no drama.
Now that you’ve got the basics down, here are some fun directions you can explore next:
- Add storage: Use AsyncStorage to save your users even after the app is closed.
- Camera integration: Let users take a photo with expo-camera instead of picking from the gallery.
- Multi-screen navigation: Create separate screens using Expo Router (e.g. user profile screen, settings).
- Backend sync: Hook up a real backend with Firebase or Supabase to store your users in the cloud.
- Styling fun: Try Tailwind (via NativeWind).
- And of course — build something you want to use. A personal habit tracker? A plant watering app? A shared family shopping list?
The important thing is: you’ve already started. The rest is just building, breaking, and learning.
You Did It — Congrats
Congrats! You’ve just created your very first mobile app with React Native and Expo. It’s no small feat, and the best part is — you’re just getting started! 🎉
The skills you learned today — state management, handling images, and rendering dynamic lists — are key building blocks for more complex apps. You’ve unlocked the ability to create and launch your own mobile experiences, and the possibilities are endless.
But remember, the journey doesn’t stop here. Every step you take, even the small ones, brings you closer to becoming an amazing developer. So keep coding, keep creating, and keep learning!
Thanks for reading, and I can’t wait to see what you build next. If you found this helpful, stick around and stay tuned for more tutorials.
See you in the next one, developer. Enjoy the journey and have fun! 💙