Skip to content

From Zero to App: Your First React Native App with Expo

Posted on:April 9, 2025

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?


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:

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:

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.

  1. 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.”

  2. 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)
  3. 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.

  4. 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.

  5. 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.

  6. 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!

  7. 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:

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:

  1. Works on both iOS and Android
  2. Reloads instantly when you save changes
  3. Feels like magic when it just works
  4. Just make sure your phone and computer are on the same Wi-Fi network, and you’re good to go!

What we just did

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:

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! 💙