DEV Community

Cover image for React Native: Zero to Hero - Part 2
Fonyuy Gita
Fonyuy Gita

Posted on

React Native: Zero to Hero - Part 2

This is Part 2 of our "React Native: Zero to Hero" series - where we dive deep into components, styling, and state management while building our first real app!


Table of Contents

  1. Welcome Back to Your React Native Journey
  2. Understanding React Native Components
  3. Building Your First UI Components
  4. Styling React Native Apps: Beyond the Basics
  5. Adding Interactivity to Your Components
  6. Managing State in React Native
  7. Exploring Layouts and Flexbox
  8. Using Flexbox to Create Responsive Layouts
  9. Improving Your Layouts with Advanced Techniques
  10. Project: Building Our Todo List App - Part 1
  11. Testing Your Todo App
  12. Troubleshooting Common Issues
  13. What You've Learned
  14. Resources and Next Steps

Welcome Back to Your React Native Journey {#welcome}

welcome back
Welcome back, future React Native developer! In Part 1, we set up our development environment and created our first app. Now we're ready to dive into the exciting world of building real, interactive mobile applications.

Today, we'll transform you from someone who can run "Hello World" into a developer who can build actual mobile apps with beautiful interfaces and interactive features. We'll do this by building a Todo List app together - a perfect project for learning the fundamentals while creating something genuinely useful.

What Makes This Tutorial Special

Unlike other tutorials that jump straight into complex concepts, we'll build your understanding step by step. Think of this as learning to cook - we won't just give you a recipe; we'll teach you why each ingredient matters and how they work together to create something delicious.

By the end of this tutorial, you'll understand not just how to write React Native code, but why it works the way it does. This foundation will serve you well as you tackle more complex projects in the future.


Understanding React Native Components {#understanding-components}

Before we start building, let's understand what components really are. If you've ever played with building blocks like LEGO, you'll love this concept!

What Are Components?

Think of components as smart building blocks for your app. Just like LEGO blocks can be combined to create castles, cars, or spaceships, React Native components can be combined to create buttons, screens, or entire applications.

react native analogies

The Component Hierarchy

Every React Native app is built as a tree of components. At the top, you have your main App component, and below it, you have smaller components that make up different parts of your interface.

App
├── Header
├── TodoList
│   ├── TodoItem
│   ├── TodoItem
│   └── TodoItem
└── Footer
Enter fullscreen mode Exit fullscreen mode

Two Types of Components

React Native has two main types of components, and understanding the difference is crucial:

Built-in Components (React Native provides these):

  • View: Like a container or div in web development
  • Text: For displaying text
  • Button: For clickable buttons
  • TextInput: For user input
  • ScrollView: For scrollable content
  • Image: For displaying images

Custom Components (You create these):
These are components you build by combining built-in components. For example, you might create a TodoItem component that combines a View, Text, and Button.

Your First Custom Component

Let's create a simple custom component to understand the concept:

// This is a custom component called Greeting
function Greeting() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Hello, React Native Developer!</Text>
    </View>
  );
}

// Styles for our component
const styles = StyleSheet.create({
  container: {
    padding: 20,
    backgroundColor: '#f0f0f0',
    borderRadius: 10,
  },
  text: {
    fontSize: 18,
    color: '#333',
  },
});
Enter fullscreen mode Exit fullscreen mode

Why Components Are Powerful

Components are powerful because they're:

  • Reusable: Write once, use many times
  • Maintainable: Easy to update and debug
  • Organized: Keep your code structured and clean
  • Testable: Test individual pieces of your app

Building Your First UI Components {#building-ui-components}

Now let's get our hands dirty building real UI components. We'll start simple and gradually add complexity.

Component Building Blocks

Every component in React Native is built using these fundamental pieces:

JSX (JavaScript XML):
This is the syntax that looks like HTML but is actually JavaScript. It's how we describe what our component should look like.

// This JSX...
<View>
  <Text>Hello World</Text>
</View>

// ...is actually this JavaScript:
React.createElement(
  View,
  null,
  React.createElement(Text, null, "Hello World")
);
Enter fullscreen mode Exit fullscreen mode

Props (Properties):
These are like function parameters - they let you pass data into your components.

function WelcomeMessage({ name, age }) {
  return (
    <View>
      <Text>Welcome, {name}!</Text>
      <Text>You are {age} years old.</Text>
    </View>
  );
}

// Using the component
<WelcomeMessage name="Sarah" age={25} />
Enter fullscreen mode Exit fullscreen mode

State:
This is data that can change over time. When state changes, your component re-renders to show the new data.

Building a Profile Card Component

Let's build a more complex component to see these concepts in action:

import React from 'react';
import { View, Text, StyleSheet, Image } from 'react-native';

function ProfileCard({ name, role, avatar, email }) {
  return (
    <View style={styles.card}>
      <Image source={{ uri: avatar }} style={styles.avatar} />
      <View style={styles.info}>
        <Text style={styles.name}>{name}</Text>
        <Text style={styles.role}>{role}</Text>
        <Text style={styles.email}>{email}</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    flexDirection: 'row',
    backgroundColor: 'white',
    borderRadius: 10,
    padding: 15,
    margin: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3, // For Android shadow
  },
  avatar: {
    width: 60,
    height: 60,
    borderRadius: 30,
    marginRight: 15,
  },
  info: {
    flex: 1,
    justifyContent: 'center',
  },
  name: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333',
  },
  role: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
  email: {
    fontSize: 12,
    color: '#999',
    marginTop: 2,
  },
});

export default ProfileCard;
Enter fullscreen mode Exit fullscreen mode

Understanding the Component Structure

Let's break down what makes this component work:

The Function Declaration:

function ProfileCard({ name, role, avatar, email }) {
Enter fullscreen mode Exit fullscreen mode

This creates a function that accepts props as parameters. We use destructuring to extract the specific props we need.

The JSX Return:

return (
  <View style={styles.card}>
    {/* Component content */}
  </View>
);
Enter fullscreen mode Exit fullscreen mode

Every component must return JSX that describes what should be rendered.

Props Usage:

<Text style={styles.name}>{name}</Text>
Enter fullscreen mode Exit fullscreen mode

We use curly braces {} to insert JavaScript expressions into our JSX.


Styling React Native Apps: Beyond the Basics {#styling-rn-apps}

Styling in React Native is different from web CSS, but once you understand the concepts, it becomes intuitive and powerful.

The StyleSheet API

React Native uses the StyleSheet API to create styles. Think of it as CSS-in-JavaScript with some mobile-specific enhancements.

import { StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
  },
});
Enter fullscreen mode Exit fullscreen mode

Why Use StyleSheet Instead of Inline Styles?

While you can use inline styles, StyleSheet offers several advantages:

Performance Benefits:
StyleSheet optimizes styles for better performance by creating style objects only once.

Validation:
In development mode, StyleSheet validates your styles and warns you about errors.

Code Organization:
Keeps your styles organized and separate from your component logic.

Core Styling Concepts

Dimensions and Units:
React Native uses density-independent pixels (dp) as the default unit. No need to specify units like in web CSS.

const styles = StyleSheet.create({
  box: {
    width: 100,        // 100 dp
    height: 100,       // 100 dp
    padding: 10,       // 10 dp
    margin: 5,         // 5 dp
  },
});
Enter fullscreen mode Exit fullscreen mode

Colors:
React Native supports various color formats:

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ff6b6b',     // Hex
    backgroundColor: 'red',         // Named color
    backgroundColor: 'rgb(255, 0, 0)',  // RGB
    backgroundColor: 'rgba(255, 0, 0, 0.5)',  // RGBA
  },
});
Enter fullscreen mode Exit fullscreen mode

Typography:
Text styling is handled through the Text component:

const styles = StyleSheet.create({
  heading: {
    fontSize: 24,
    fontWeight: 'bold',
    fontFamily: 'Arial',
    color: '#333',
    textAlign: 'center',
    lineHeight: 30,
  },
});
Enter fullscreen mode Exit fullscreen mode

Platform-Specific Styling

Sometimes you need different styles for iOS and Android:

import { Platform } from 'react-native';

const styles = StyleSheet.create({
  container: {
    paddingTop: Platform.OS === 'ios' ? 20 : 0,
    ...Platform.select({
      ios: {
        backgroundColor: '#f0f0f0',
      },
      android: {
        backgroundColor: '#ffffff',
      },
    }),
  },
});
Enter fullscreen mode Exit fullscreen mode

Creating Reusable Style Objects

You can create reusable style objects to maintain consistency:

const colors = {
  primary: '#007AFF',
  secondary: '#FF6B6B',
  background: '#F5F5F5',
  text: '#333333',
};

const typography = {
  heading: {
    fontSize: 24,
    fontWeight: 'bold',
  },
  body: {
    fontSize: 16,
    fontWeight: 'normal',
  },
};

const styles = StyleSheet.create({
  title: {
    ...typography.heading,
    color: colors.text,
  },
  button: {
    backgroundColor: colors.primary,
    padding: 15,
    borderRadius: 8,
  },
});
Enter fullscreen mode Exit fullscreen mode

Adding Interactivity to Your Components {#adding-interactivity}

Static components are nice, but interactive components make your app come alive! Let's learn how to handle user interactions.

Understanding Events in React Native

Events are actions that users perform, like tapping a button, typing in a text field, or scrolling through a list. React Native provides several ways to handle these events.

The onPress Event

The most common event you'll use is onPress, which handles touch interactions:

import React from 'react';
import { View, Text, TouchableOpacity, Alert } from 'react-native';

function InteractiveButton() {
  const handlePress = () => {
    Alert.alert('Button Pressed!', 'You tapped the button');
  };

  return (
    <TouchableOpacity onPress={handlePress} style={styles.button}>
      <Text style={styles.buttonText}>Tap Me!</Text>
    </TouchableOpacity>
  );
}
Enter fullscreen mode Exit fullscreen mode

Different Types of Touchable Components

React Native provides several touchable components for different use cases:

TouchableOpacity:
Reduces opacity when pressed - great for buttons.

<TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
  <Text>Press me</Text>
</TouchableOpacity>
Enter fullscreen mode Exit fullscreen mode

TouchableHighlight:
Shows a highlight when pressed.

<TouchableHighlight onPress={handlePress} underlayColor="#ddd">
  <Text>Press me</Text>
</TouchableHighlight>
Enter fullscreen mode Exit fullscreen mode

TouchableWithoutFeedback:
No visual feedback, but handles touch events.

<TouchableWithoutFeedback onPress={handlePress}>
  <View>
    <Text>Press me</Text>
  </View>
</TouchableWithoutFeedback>
Enter fullscreen mode Exit fullscreen mode

Handling Text Input

Text input is crucial for interactive apps. Here's how to handle it:

import React, { useState } from 'react';
import { View, TextInput, Text } from 'react-native';

function TextInputExample() {
  const [text, setText] = useState('');

  return (
    <View>
      <TextInput
        style={styles.input}
        placeholder="Type something..."
        value={text}
        onChangeText={setText}
      />
      <Text>You typed: {text}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    padding: 10,
    borderRadius: 5,
    fontSize: 16,
  },
});
Enter fullscreen mode Exit fullscreen mode

Event Handler Best Practices

Always Use Arrow Functions or useCallback:
This prevents unnecessary re-renders and maintains proper context.

// Good
const handlePress = () => {
  console.log('Button pressed');
};

// Better (for complex handlers)
const handlePress = useCallback(() => {
  console.log('Button pressed');
}, []);
Enter fullscreen mode Exit fullscreen mode

Pass Data to Event Handlers:
Sometimes you need to pass data to your event handlers.

function TodoItem({ item, onToggle, onDelete }) {
  return (
    <View>
      <TouchableOpacity onPress={() => onToggle(item.id)}>
        <Text>{item.text}</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onDelete(item.id)}>
        <Text>Delete</Text>
      </TouchableOpacity>
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Managing State in React Native {#managing-state}

State is the heart of interactive applications. It's how your app remembers information and responds to user actions.

What is State?

State is data that can change over time. When state changes, React Native automatically re-renders your component to show the new data. Think of state as your app's memory.

Understanding useState Hook

The useState hook is the most common way to manage state in functional components:

import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';

function Counter() {
  // useState returns an array with two elements:
  // 1. The current state value
  // 2. A function to update the state
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.countText}>Count: {count}</Text>
      <Button title="+" onPress={increment} />
      <Button title="-" onPress={decrement} />
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

State Management Patterns

Simple State:
For basic values like numbers, strings, or booleans.

const [isLoading, setIsLoading] = useState(false);
const [userName, setUserName] = useState('');
const [age, setAge] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Object State:
For more complex data structures.

const [user, setUser] = useState({
  name: '',
  email: '',
  age: 0,
});

// Updating object state
const updateUserName = (newName) => {
  setUser({
    ...user,  // Spread the existing user object
    name: newName,  // Update only the name
  });
};
Enter fullscreen mode Exit fullscreen mode

Array State:
For lists of data.

const [todos, setTodos] = useState([]);

// Adding an item
const addTodo = (newTodo) => {
  setTodos([...todos, newTodo]);
};

// Removing an item
const removeTodo = (todoId) => {
  setTodos(todos.filter(todo => todo.id !== todoId));
};

// Updating an item
const updateTodo = (todoId, updates) => {
  setTodos(todos.map(todo => 
    todo.id === todoId ? { ...todo, ...updates } : todo
  ));
};
Enter fullscreen mode Exit fullscreen mode

State Update Rules

State Updates Are Asynchronous:
State updates don't happen immediately. React batches them for performance.

const [count, setCount] = useState(0);

const handlePress = () => {
  setCount(count + 1);
  console.log(count); // This will still show the old value
};
Enter fullscreen mode Exit fullscreen mode

Always Use the Setter Function:
Never modify state directly. Always use the setter function.

// Wrong
count = count + 1;

// Correct
setCount(count + 1);
Enter fullscreen mode Exit fullscreen mode

Use Functional Updates for Dependent State:
When your new state depends on the previous state, use a function.

// Good
setCount(count + 1);

// Better (prevents race conditions)
setCount(prevCount => prevCount + 1);
Enter fullscreen mode Exit fullscreen mode

Exploring Layouts and Flexbox {#exploring-layouts}

Layout is how you arrange components on the screen. React Native uses Flexbox, a powerful layout system that makes it easy to create responsive designs.

Understanding Flexbox

Flexbox is a layout method that arranges items in a container (called a flex container) in a predictable way. Imagine you have a box (container) and you want to arrange smaller boxes (items) inside it.

flex box in react native

The Main Concepts

Flex Container:
Any component with display: 'flex' (which is the default for View components) becomes a flex container.

Flex Items:
The children of a flex container are called flex items.

Main Axis and Cross Axis:

  • Main axis: The primary axis along which flex items are laid out
  • Cross axis: The perpendicular axis to the main axis

Basic Flexbox Properties

flex:
This property defines how much space an item should take up.

const styles = StyleSheet.create({
  container: {
    flex: 1,  // Takes up all available space
    flexDirection: 'row',
  },
  item1: {
    flex: 1,  // Takes up 1/3 of the space
    backgroundColor: 'red',
  },
  item2: {
    flex: 2,  // Takes up 2/3 of the space
    backgroundColor: 'blue',
  },
});
Enter fullscreen mode Exit fullscreen mode

flexDirection:
Determines the direction of the main axis.

const styles = StyleSheet.create({
  row: {
    flexDirection: 'row',     // Horizontal layout
  },
  column: {
    flexDirection: 'column',  // Vertical layout (default)
  },
  rowReverse: {
    flexDirection: 'row-reverse',  // Horizontal, but reversed
  },
  columnReverse: {
    flexDirection: 'column-reverse',  // Vertical, but reversed
  },
});
Enter fullscreen mode Exit fullscreen mode

justifyContent:
Aligns items along the main axis.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',    // Start of the main axis
    justifyContent: 'flex-end',      // End of the main axis
    justifyContent: 'center',        // Center of the main axis
    justifyContent: 'space-between', // Equal space between items
    justifyContent: 'space-around',  // Equal space around items
    justifyContent: 'space-evenly',  // Equal space everywhere
  },
});
Enter fullscreen mode Exit fullscreen mode

alignItems:
Aligns items along the cross axis.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'flex-start',  // Start of the cross axis
    alignItems: 'flex-end',    // End of the cross axis
    alignItems: 'center',      // Center of the cross axis
    alignItems: 'stretch',     // Stretch to fill the cross axis
  },
});
Enter fullscreen mode Exit fullscreen mode

Practical Layout Examples

Centering Content:

const styles = StyleSheet.create({
  centerEverything: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});
Enter fullscreen mode Exit fullscreen mode

Header, Content, Footer Layout:

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    height: 60,
    backgroundColor: '#007AFF',
    justifyContent: 'center',
    alignItems: 'center',
  },
  content: {
    flex: 1,
    padding: 20,
  },
  footer: {
    height: 50,
    backgroundColor: '#f0f0f0',
    justifyContent: 'center',
    alignItems: 'center',
  },
});
Enter fullscreen mode Exit fullscreen mode

Two-Column Layout:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
  },
  leftColumn: {
    flex: 1,
    backgroundColor: '#f0f0f0',
    padding: 10,
  },
  rightColumn: {
    flex: 2,
    backgroundColor: '#e0e0e0',
    padding: 10,
  },
});
Enter fullscreen mode Exit fullscreen mode

Using Flexbox to Create Responsive Layouts {#using-flexbox}

Responsive design ensures your app looks great on different screen sizes. Let's explore how to create layouts that adapt to various devices.

Understanding Screen Dimensions

React Native provides tools to get screen dimensions:

import { Dimensions } from 'react-native';

const { width, height } = Dimensions.get('window');
const screenData = Dimensions.get('screen');

console.log('Window width:', width);
console.log('Window height:', height);
Enter fullscreen mode Exit fullscreen mode

Responsive Design Patterns

Percentage-Based Layouts:
Use percentages instead of fixed dimensions.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: '5%',  // 5% of the container's width
  },
  button: {
    width: '80%',   // 80% of the container's width
    height: 50,
  },
});
Enter fullscreen mode Exit fullscreen mode

Aspect Ratio Layouts:
Maintain consistent proportions across devices.

const styles = StyleSheet.create({
  imageContainer: {
    width: '100%',
    aspectRatio: 16/9,  // 16:9 aspect ratio
  },
  image: {
    width: '100%',
    height: '100%',
  },
});
Enter fullscreen mode Exit fullscreen mode

Conditional Layouts:
Show different layouts based on screen size.

import { Dimensions } from 'react-native';

function ResponsiveLayout() {
  const { width } = Dimensions.get('window');
  const isTablet = width > 768;

  return (
    <View style={[
      styles.container,
      isTablet ? styles.tabletLayout : styles.phoneLayout
    ]}>
      {/* Your content */}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  phoneLayout: {
    flexDirection: 'column',
  },
  tabletLayout: {
    flexDirection: 'row',
  },
});
Enter fullscreen mode Exit fullscreen mode

Advanced Flexbox Techniques

Flexible Spacing:
Use flex to create flexible spacing between elements.

const styles = StyleSheet.create({
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 15,
  },
  title: {
    flex: 1,  // Takes up available space
    fontSize: 18,
    fontWeight: 'bold',
  },
  // The button will only take up as much space as it needs
});
Enter fullscreen mode Exit fullscreen mode

Wrapping Content:
Allow items to wrap to new lines.

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'space-between',
  },
  item: {
    width: '48%',  // Two items per row with some spacing
    marginBottom: 10,
  },
});
Enter fullscreen mode Exit fullscreen mode

Nested Flex Containers:
Create complex layouts by nesting flex containers.

const styles = StyleSheet.create({
  outerContainer: {
    flex: 1,
    flexDirection: 'column',
  },
  innerContainer: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  item: {
    flex: 1,
    margin: 5,
  },
});
Enter fullscreen mode Exit fullscreen mode

Improving Your Layouts with Advanced Techniques {#improving-layouts}

Now that you understand the basics, let's explore advanced techniques to make your layouts more polished and professional.

Spacing and Padding Strategies

Consistent Spacing:
Use a consistent spacing system throughout your app.

const spacing = {
  xs: 4,
  sm: 8,
  md: 16,
  lg: 24,
  xl: 32,
};

const styles = StyleSheet.create({
  container: {
    padding: spacing.md,
  },
  title: {
    marginBottom: spacing.sm,
  },
  button: {
    marginTop: spacing.lg,
  },
});
Enter fullscreen mode Exit fullscreen mode

Negative Margins:
Use negative margins to create overlapping effects.

const styles = StyleSheet.create({
  card: {
    backgroundColor: 'white',
    borderRadius: 10,
    padding: 20,
    marginTop: -10,  // Overlaps with the element above
  },
});
Enter fullscreen mode Exit fullscreen mode

Shadows and Elevation

iOS Shadows:

const styles = StyleSheet.create({
  cardWithShadow: {
    backgroundColor: 'white',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
});
Enter fullscreen mode Exit fullscreen mode

Android Elevation:

const styles = StyleSheet.create({
  cardWithElevation: {
    backgroundColor: 'white',
    elevation: 3,
  },
});
Enter fullscreen mode Exit fullscreen mode

Cross-Platform Shadows:

const styles = StyleSheet.create({
  card: {
    backgroundColor: 'white',
    borderRadius: 10,
    padding: 15,
    // iOS shadow
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    // Android elevation
    elevation: 3,
  },
});
Enter fullscreen mode Exit fullscreen mode

Borders and Decorative Elements

Border Radius:
Create rounded corners with different radii.

const styles = StyleSheet.create({
  button: {
    borderRadius: 25,           // Fully rounded
    borderTopLeftRadius: 10,    // Individual corners
    borderTopRightRadius: 10,
    borderBottomLeftRadius: 0,
    borderBottomRightRadius: 0,
  },
});
Enter fullscreen mode Exit fullscreen mode

Borders:
Add borders for visual separation.

const styles = StyleSheet.create({
  container: {
    borderWidth: 1,
    borderColor: '#ddd',
    borderStyle: 'solid',
    borderLeftWidth: 3,         // Different widths for different sides
    borderLeftColor: '#007AFF',
  },
});
Enter fullscreen mode Exit fullscreen mode

Position and Layout Control

Absolute Positioning:
Position elements relative to their parent.

const styles = StyleSheet.create({
  container: {
    position: 'relative',
    height: 200,
  },
  floatingButton: {
    position: 'absolute',
    bottom: 20,
    right: 20,
    width: 60,
    height: 60,
    borderRadius: 30,
    backgroundColor: '#007AFF',
  },
});
Enter fullscreen mode Exit fullscreen mode

Z-Index:
Control the stacking order of elements.

const styles = StyleSheet.create({
  background: {
    zIndex: 1,
  },
  foreground: {
    zIndex: 2,
  },
});
Enter fullscreen mode Exit fullscreen mode

Project: Building Our Todo List App - Part 1 {#todo-app-part1}

Now let's put everything we've learned together by building a real Todo List app! This will be the foundation for our three-part Todo app series.

Project Overview

Our Todo List app will have the following features:

  • Add new todos
  • Display a list of todos
  • Mark todos as complete
  • Delete todos
  • Basic styling and layout

Setting Up the Project Structure

First, let's create the basic structure for our Todo app:

// App.js
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  FlatList,
  StyleSheet,
  SafeAreaView,
  StatusBar,
} from 'react-native';

export default function App() {
  // We'll build this step by step
  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      <Text style={styles.title}>My Todo List</Text>
      {/* We'll add more components here */}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
    paddingTop: StatusBar.currentHeight || 0,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    textAlign: 'center',
    marginVertical: 20,
    color: '#2c3e50',
  },
});
Enter fullscreen mode Exit fullscreen mode

Creating the Todo Data Structure

Let's define how our todos will be structured:

// Each todo will be an object with these properties:
const todoExample = {
  id: '1',                    // Unique identifier
  text: 'Learn React Native', // The todo text
  completed: false,           // Whether it's completed
  createdAt: new Date(),      // When it was created
};
Enter fullscreen mode Exit fullscreen mode

Building the Add Todo Component (continued)

Let's continue building our Add Todo component where we left off:

function AddTodoForm({ onAddTodo }) {
  const [text, setText] = useState('');

  const handleSubmit = () => {
    // Don't add empty todos
    if (text.trim() === '') {
      return;
    }

    // Create a new todo object
    const newTodo = {
      id: Date.now().toString(), // Simple ID generation
      text: text.trim(),
      completed: false,
      createdAt: new Date(),
    };

    // Call the parent function to add the todo
    onAddTodo(newTodo);

    // Clear the input field
    setText('');
  };

  return (
    <View style={styles.addTodoContainer}>
      <TextInput
        style={styles.textInput}
        placeholder="Enter a new todo..."
        value={text}
        onChangeText={setText}
        onSubmitEditing={handleSubmit} // Allow submit by pressing Enter
        returnKeyType="done"
      />
      <TouchableOpacity 
        style={styles.addButton} 
        onPress={handleSubmit}
        disabled={text.trim() === ''} // Disable button if input is empty
      >
        <Text style={styles.addButtonText}>Add</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  // ... previous styles ...

  addTodoContainer: {
    flexDirection: 'row',
    margin: 20,
    marginBottom: 10,
  },
  textInput: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 15,
    paddingVertical: 12,
    fontSize: 16,
    backgroundColor: 'white',
    marginRight: 10,
  },
  addButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 20,
    paddingVertical: 12,
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});
Enter fullscreen mode Exit fullscreen mode

Notice how we're using several important React Native concepts here. The onChangeText prop connects the input field to our state, creating what we call a "controlled component." This means React is managing the input's value, which gives us more control over the user experience.

The onSubmitEditing prop allows users to add a todo by pressing the "Done" button on their keyboard, which creates a more intuitive user experience. We're also using the disabled prop to prevent users from adding empty todos, which demonstrates how we can provide immediate feedback based on the current state.

Creating the Todo Item Component

Now let's build the individual todo item component. This component will display each todo and handle the completion and deletion actions:

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <View style={styles.todoItem}>
      <TouchableOpacity 
        style={[styles.todoContent, todo.completed && styles.todoCompleted]}
        onPress={() => onToggle(todo.id)}
      >
        <Text style={[
          styles.todoText, 
          todo.completed && styles.todoTextCompleted
        ]}>
          {todo.text}
        </Text>
        <Text style={styles.todoDate}>
          {formatDate(todo.createdAt)}
        </Text>
      </TouchableOpacity>

      <TouchableOpacity 
        style={styles.deleteButton}
        onPress={() => onDelete(todo.id)}
      >
        <Text style={styles.deleteButtonText}>Delete</Text>
      </TouchableOpacity>
    </View>
  );
}

// Helper function to format dates nicely
function formatDate(date) {
  const now = new Date();
  const todoDate = new Date(date);
  const diffTime = now - todoDate;
  const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));

  if (diffDays === 0) {
    return 'Today';
  } else if (diffDays === 1) {
    return 'Yesterday';
  } else if (diffDays < 7) {
    return `${diffDays} days ago`;
  } else {
    return todoDate.toLocaleDateString();
  }
}

const styles = StyleSheet.create({
  // ... previous styles ...

  todoItem: {
    flexDirection: 'row',
    backgroundColor: 'white',
    marginHorizontal: 20,
    marginVertical: 5,
    borderRadius: 8,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    elevation: 2,
  },
  todoContent: {
    flex: 1,
    padding: 15,
  },
  todoCompleted: {
    opacity: 0.6,
  },
  todoText: {
    fontSize: 16,
    color: '#2c3e50',
    marginBottom: 4,
  },
  todoTextCompleted: {
    textDecorationLine: 'line-through',
    color: '#7f8c8d',
  },
  todoDate: {
    fontSize: 12,
    color: '#95a5a6',
  },
  deleteButton: {
    backgroundColor: '#e74c3c',
    paddingHorizontal: 15,
    paddingVertical: 10,
    borderTopRightRadius: 8,
    borderBottomRightRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
  },
  deleteButtonText: {
    color: 'white',
    fontSize: 14,
    fontWeight: '600',
  },
});
Enter fullscreen mode Exit fullscreen mode

This component demonstrates several advanced styling techniques. We're using conditional styling with the array syntax [styles.todoText, todo.completed && styles.todoTextCompleted], which applies different styles based on the todo's completion state. This is a common pattern in React Native for dynamic styling.

The formatDate function shows how we can create utility functions to handle common tasks like date formatting. This keeps our components clean and focused on their primary purpose while providing a better user experience.

Managing Todo State in the Main App

Now let's update our main App component to manage the todo state and connect all our components:

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

  // Function to add a new todo
  const addTodo = (newTodo) => {
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

  // Function to toggle a todo's completion status
  const toggleTodo = (todoId) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === todoId 
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };

  // Function to delete a todo
  const deleteTodo = (todoId) => {
    setTodos(prevTodos => 
      prevTodos.filter(todo => todo.id !== todoId)
    );
  };

  // Function to render each todo item
  const renderTodoItem = ({ item }) => (
    <TodoItem 
      todo={item}
      onToggle={toggleTodo}
      onDelete={deleteTodo}
    />
  );

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />

      <Text style={styles.title}>My Todo List</Text>

      <AddTodoForm onAddTodo={addTodo} />

      {todos.length === 0 ? (
        <View style={styles.emptyState}>
          <Text style={styles.emptyStateText}>
            No todos yet. Add one above to get started!
          </Text>
        </View>
      ) : (
        <FlatList
          data={todos}
          renderItem={renderTodoItem}
          keyExtractor={(item) => item.id}
          style={styles.todoList}
          showsVerticalScrollIndicator={false}
        />
      )}

      <View style={styles.footer}>
        <Text style={styles.footerText}>
          {todos.filter(todo => !todo.completed).length} of {todos.length} remaining
        </Text>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  // ... previous styles ...

  emptyState: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 40,
  },
  emptyStateText: {
    fontSize: 16,
    color: '#7f8c8d',
    textAlign: 'center',
    lineHeight: 24,
  },
  todoList: {
    flex: 1,
  },
  footer: {
    padding: 20,
    paddingTop: 10,
    alignItems: 'center',
  },
  footerText: {
    fontSize: 14,
    color: '#7f8c8d',
  },
});
Enter fullscreen mode Exit fullscreen mode

This main component demonstrates several important state management patterns. We're using functional state updates with prevTodos => to ensure our state updates are based on the current state, which prevents race conditions and makes our app more reliable.

The FlatList component is perfect for rendering lists of data efficiently. It only renders the items that are currently visible on screen, which makes it performant even with large lists. The keyExtractor prop tells React Native how to identify each item uniquely, which is crucial for efficient re-rendering.

Adding Polish with Enhanced Styling

Let's add some finishing touches to make our app look more professional:

// Enhanced styles for a more polished look
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
    paddingTop: StatusBar.currentHeight || 0,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    textAlign: 'center',
    marginVertical: 20,
    color: '#2c3e50',
    letterSpacing: 0.5,
  },
  addTodoContainer: {
    flexDirection: 'row',
    margin: 20,
    marginBottom: 10,
  },
  textInput: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 15,
    paddingVertical: 12,
    fontSize: 16,
    backgroundColor: 'white',
    marginRight: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    elevation: 1,
  },
  addButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 20,
    paddingVertical: 12,
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#007AFF',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 3,
  },
  addButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
    letterSpacing: 0.5,
  },
  todoItem: {
    flexDirection: 'row',
    backgroundColor: 'white',
    marginHorizontal: 20,
    marginVertical: 5,
    borderRadius: 8,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 2,
    overflow: 'hidden', // Ensures children respect border radius
  },
  todoContent: {
    flex: 1,
    padding: 15,
  },
  todoCompleted: {
    opacity: 0.6,
    backgroundColor: '#f8f9fa',
  },
  todoText: {
    fontSize: 16,
    color: '#2c3e50',
    marginBottom: 4,
    lineHeight: 22,
  },
  todoTextCompleted: {
    textDecorationLine: 'line-through',
    color: '#7f8c8d',
  },
  todoDate: {
    fontSize: 12,
    color: '#95a5a6',
  },
  deleteButton: {
    backgroundColor: '#e74c3c',
    paddingHorizontal: 15,
    paddingVertical: 10,
    justifyContent: 'center',
    alignItems: 'center',
    minWidth: 80,
  },
  deleteButtonText: {
    color: 'white',
    fontSize: 14,
    fontWeight: '600',
  },
  emptyState: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 40,
  },
  emptyStateText: {
    fontSize: 16,
    color: '#7f8c8d',
    textAlign: 'center',
    lineHeight: 24,
  },
  todoList: {
    flex: 1,
    paddingTop: 10,
  },
  footer: {
    padding: 20,
    paddingTop: 10,
    alignItems: 'center',
    borderTopWidth: 1,
    borderTopColor: '#ecf0f1',
  },
  footerText: {
    fontSize: 14,
    color: '#7f8c8d',
    fontWeight: '500',
  },
});
Enter fullscreen mode Exit fullscreen mode

These enhanced styles demonstrate several professional design principles. We've added subtle shadows to create depth, used consistent spacing throughout the app, and added visual hierarchy with different font weights and sizes. The overflow: 'hidden' property ensures that child elements respect their parent's border radius, which is important for creating clean, professional-looking interfaces.

Testing Your Todo App {#testing-todo-app}

Testing is crucial for ensuring your app works correctly. Let's explore different ways to test your Todo app during development.

Manual Testing Strategies

When testing your Todo app, you should systematically check each feature to ensure it works as expected. Start by testing the basic functionality: adding todos, marking them as complete, and deleting them. Try edge cases like adding empty todos, very long todo text, and rapid consecutive actions.

Here's a comprehensive testing checklist for your Todo app:

Adding Todos:

  • Try adding a normal todo
  • Attempt to add an empty todo (should be prevented)
  • Add a very long todo text
  • Test the keyboard submit functionality
  • Verify the input clears after adding

Managing Todos:

  • Toggle completion status multiple times
  • Delete todos from different positions in the list
  • Test with an empty list
  • Test with many todos to check scrolling

Visual Testing:

  • Check the app on different screen sizes
  • Verify that completed todos look different from active ones
  • Ensure the footer count updates correctly
  • Test the empty state message

Using React Native Debugger

React Native provides excellent debugging tools. You can enable the developer menu by shaking your device or pressing Cmd+D (iOS) or Ctrl+M (Android) in the simulator. The remote debugger allows you to use Chrome DevTools to inspect your app's state and behavior.

To debug your Todo app effectively, add console logs to your state management functions:

const addTodo = (newTodo) => {
  console.log('Adding todo:', newTodo);
  setTodos(prevTodos => {
    const newTodos = [...prevTodos, newTodo];
    console.log('New todos array:', newTodos);
    return newTodos;
  });
};

const toggleTodo = (todoId) => {
  console.log('Toggling todo:', todoId);
  setTodos(prevTodos =>
    prevTodos.map(todo => {
      if (todo.id === todoId) {
        console.log('Toggling todo:', todo.text, 'from', todo.completed, 'to', !todo.completed);
        return { ...todo, completed: !todo.completed };
      }
      return todo;
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

As your Todo list grows, you might notice performance issues. The FlatList component is already optimized for large lists, but you can further improve performance by implementing React.memo for your TodoItem component:

const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
  return (
    <View style={styles.todoItem}>
      {/* Component content */}
    </View>
  );
});
Enter fullscreen mode Exit fullscreen mode

This prevents unnecessary re-renders when the todo data hasn't changed, which is especially important when you have many todos in your list.

Troubleshooting Common Issues {#troubleshooting}

Let's address some common issues you might encounter while building your Todo app and how to solve them.

State Not Updating Issues

One of the most common issues beginners face is state not updating as expected. This usually happens when you try to modify state directly instead of using the setter function:

// Wrong - this won't trigger a re-render
todos.push(newTodo);

// Wrong - this mutates the existing array
todos[0].completed = true;

// Correct - create a new array
setTodos([...todos, newTodo]);

// Correct - create a new array with updated item
setTodos(todos.map(todo => 
  todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
));
Enter fullscreen mode Exit fullscreen mode

The key principle is that you should always create new objects or arrays when updating state, never modify existing ones. This is because React uses a process called "reconciliation" to determine when to re-render components, and it relies on detecting changes in state references.

Styling Issues

Common styling problems include elements not appearing where expected or not having the right dimensions. Remember that React Native uses Flexbox by default, so understanding the flex properties is crucial:

// Common issue: Element doesn't expand to fill space
const styles = StyleSheet.create({
  container: {
    flex: 1, // This makes the container expand to fill available space
  },
  // Without flex: 1, the container only takes up as much space as its content needs
});
Enter fullscreen mode Exit fullscreen mode

Another common issue is forgetting that View components don't have a background color by default, which can make debugging layout issues difficult. Always set a background color when you're trying to understand how your layout is working.

Performance Problems

If your app becomes slow with many todos, consider these optimizations:

Implement proper key extraction:

// Good - stable, unique keys
<FlatList
  data={todos}
  keyExtractor={(item) => item.id}
  renderItem={renderTodoItem}
/>

// Bad - index-based keys can cause issues
<FlatList
  data={todos}
  keyExtractor={(item, index) => index.toString()}
  renderItem={renderTodoItem}
/>
Enter fullscreen mode Exit fullscreen mode

Use getItemLayout for better performance:
If your todo items have a consistent height, you can provide this information to FlatList for better performance:

const getItemLayout = (data, index) => ({
  length: 70, // Height of each item
  offset: 70 * index,
  index,
});

<FlatList
  data={todos}
  getItemLayout={getItemLayout}
  renderItem={renderTodoItem}
  keyExtractor={(item) => item.id}
/>
Enter fullscreen mode Exit fullscreen mode

Platform-Specific Issues

Sometimes your app might behave differently on iOS and Android. Use the Platform API to handle these differences:

import { Platform } from 'react-native';

const styles = StyleSheet.create({
  container: {
    paddingTop: Platform.OS === 'ios' ? 20 : StatusBar.currentHeight,
  },
  text: {
    fontFamily: Platform.OS === 'ios' ? 'Helvetica' : 'Roboto',
  },
});
Enter fullscreen mode Exit fullscreen mode

What You've Learned {#what-learned}

Congratulations! You've built a fully functional Todo List app and learned fundamental React Native concepts along the way. Let's review what you've accomplished:

Component Architecture: You understand how to break down your app into reusable components. The TodoItem component can be used anywhere in your app, and the AddTodoForm handles a specific piece of functionality independently.

State Management: You've learned how to manage complex state with arrays and objects, how to update state immutably, and how to pass state between components through props and callback functions.

Event Handling: You can handle user interactions like button presses and text input changes. You understand how to prevent default behaviors and provide immediate feedback to users.

Layout and Styling: You've mastered Flexbox layouts and can create responsive designs that work on different screen sizes. You understand how to use StyleSheet for performance and organization.

List Rendering: You can efficiently render large lists of data using FlatList and understand key concepts like key extraction and item layout optimization.

User Experience: You've learned how to create empty states, provide visual feedback for different states (completed vs. active todos), and display helpful information like todo counts.

Debugging and Testing: You understand how to test your app manually and use React Native's debugging tools to identify and fix issues.

These skills form the foundation for building more complex React Native applications. You can now create interactive UIs, manage application state, and provide great user experiences across different devices and platforms.

Resources and Next Steps {#resources-next-steps}

Now that you've built your first real React Native app, here are some resources and next steps to continue your learning journey:

Essential Documentation

The React Native documentation is your best friend for learning new components and APIs. Focus on these sections:

  • Core Components and APIs: Learn about ScrollView, Modal, Alert, and other essential components
  • Navigation: Understand how to create multi-screen apps with React Navigation
  • Networking: Learn how to fetch data from APIs and handle loading states
  • Platform-specific Code: Understand how to create platform-specific experiences

Recommended Learning Path

Immediate Next Steps:

  1. Add persistence to your Todo app using AsyncStorage so todos don't disappear when the app closes
  2. Implement filtering (show all, active, completed todos)
  3. Add the ability to edit existing todos
  4. Implement todo categories or tags

Intermediate Projects:

  1. Weather app with API integration
  2. Photo gallery app using device camera
  3. Simple social media app with user authentication
  4. E-commerce app with product lists and cart functionality

Advanced Topics to Explore:

  1. State management with Redux or Context API
  2. Animation and gesture handling
  3. Push notifications
  4. App deployment to app stores
  5. Performance optimization techniques

Community and Learning Resources

The React Native community is vibrant and helpful. Join these communities to get help and stay updated:

  • React Native Discord: Real-time chat with other developers
  • Reddit r/reactnative: Discussion forum with tutorials and help
  • Stack Overflow: Great for specific technical questions
  • GitHub: Explore open-source React Native projects to learn from others

Building Your Portfolio

As you continue learning, build projects that demonstrate your skills:

  1. Start small: Build variations of common apps (calculator, weather, notes)
  2. Focus on quality: A few well-built apps are better than many basic ones
  3. Document your work: Write about your development process and lessons learned
  4. Open source: Share your code on GitHub to get feedback and help others

Preparing for Production

When you're ready to build production apps, you'll need to learn about:

  • Code organization: Folder structure and file naming conventions
  • Error handling: Graceful error handling and user feedback
  • Testing: Unit testing, integration testing, and end-to-end testing
  • Performance: Optimization techniques and monitoring tools
  • Security: Protecting user data and preventing common vulnerabilities

The journey from beginner to professional React Native developer is exciting and rewarding. You've already taken the most important step by building your first real app. Keep practicing, stay curious, and don't be afraid to tackle projects that challenge you. Each project you build will deepen your understanding and make you a more capable developer.

Remember, every expert was once a beginner. The skills you've learned today are the foundation for building amazing mobile applications that can reach millions of users worldwide. Keep building, keep learning, and most importantly, keep having fun with React Native!

Your Todo List app is just the beginning. With these fundamentals under your belt, you're ready to tackle more complex challenges and build the mobile apps of your dreams.

Top comments (0)