How to setup React Native and create a simple To-Do App

Set up the development environment by installing Expo CLI (https://reactnative.dev/docs/environment-setup)

yarn global add expo-cli

Create a new project (app):

expo init todo-app
# select "blank (TypeScript)"

Start the development server:

cd todo-app
yarn start

# press w to open the web app

Create a To-Do app

Prerequisites

Install a checkbox component:

yarn add react-native-bouncy-checkbox

Then create the following folders & files structure:

./todo-app/
   ...
   models/
      todo.model.ts
   components/
      todo.tsx
      todo-list.tsx
      menu.tsx
   views/
      edit-todo.view.tsx
   ...

The Data Model

Define the data model in the file models/todo.model.ts:

interface ITodo {
  id: string
  done: boolean
  text: string
  color: string
}

export default ITodo

Basic components

Create the Todo component in components/todo.tsx:


import React, { useState } from "react";
import { StyleSheet, Text, View } from 'react-native';
import BouncyCheckbox from "react-native-bouncy-checkbox";
import ITodo from "../models/todo.model";

interface ITodoProps {
  data: ITodo
}

const Todo = (props: ITodoProps) => {
  const [isDone, setDone] = useState(false);
  
  return (
    <View style={[styles.container, {backgroundColor: props.data.color}]}>
       <BouncyCheckbox
          fillColor="black"
          unfillColor="#FFFFFF"
          iconStyle={{ borderColor: "black" }}
          isChecked={isDone}
          onPress={setDone}
          style={styles.checkbox}
        />
      
      <Text style={styles.text}>{props.data.text}</Text>
    </View>
  );
}

export default Todo

const styles = StyleSheet.create({
  container: {
    width: '100%', minHeight: '30px', height: 'auto',
    color: 'black',
    alignItems: 'center',
    justifyContent: 'flex-start',
    display: 'flex',
    flexDirection: 'row',
    marginBottom: 5,
    padding: 10,
    borderRadius: 5,
  },
  checkbox: {
    width: 40, 
    minWidth: 40, height: 40,
  },
  text: {
    color: 'black',
    width: '100%',
  }
});

Then, the Todo List component in components/todo-list.tsx:


import React, { useState } from "react";
import { StyleSheet, FlatList } from 'react-native';
import ITodo from "../models/todo.model";
import Todo from "./todo";

interface ITodoListProps {
  data: ITodo[]
}

const TodoList = (props: ITodoListProps) => {
  const [isDone, setDone] = useState(false);
  
  return (
    <FlatList 
        style={styles.container}
        data={props.data}
        renderItem={
          (item: any) => {
            return (
              <Todo data={item.item} />
            )
          }
        }
        keyExtractor={(item, index) => item.id}
      />
  );
}

export default TodoList

const styles = StyleSheet.create({
  container: {
    // height: '100%', maxHeight: '100%',
    height: 500,
    width: '100%',
    flexDirection: 'column',
    padding: 10,
    overflow: 'scroll',
  },
});

And the Main Menu in components/menu.tsx:


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

interface IMenu {
  onAddTodo: () => void
}

const Menu = (props: IMenu) => {  
  return (
    <View style={styles.container}>
      <Button 
        title="+ Add ToDo" 
        onPress={() => props.onAddTodo()}/>
    </View>
  );
}

export default Menu

const styles = StyleSheet.create({
  container: {
    width: '100%', minHeight: '30px', height: 'auto',
    color: 'black',
    alignItems: 'center',
    justifyContent: 'center',
    display: 'flex',
    flexDirection: 'row',
    padding: 20,
  },
});

Views

Create the Add/Edit View in view/edit-todo.view.tsx:


import React, { useState } from "react"
import { Modal, StyleSheet, TextInput, View, Text, TouchableOpacity, KeyboardAvoidingView } from 'react-native';
import ITodo from "../models/todo.model";

interface IEditTodoProps {
  isVisible: boolean
  onClose: () => void
  onSave: (data: any) => void
  data?: ITodo
}

const EditTodoView = (props: IEditTodoProps) => {

  const colors = ['#87D3F5', '#BDE991', '#BAAAFB']
  const [colorIndex, setColorIndex] = useState(0)

  const title = props.data ? 'Edit Todo' : 'Add Todo'
  const [text, setText] = useState(props.data?.text || '')


  const onSave = () => {
    if (text.trim().length === 0) {
      props.onClose()
      return
    }
    if (props.data) {
      const newData = {
        ...props.data,
        text
      }
      props.onSave(newData)
    } else {
      const newData = {
        id: 'id-' + Math.floor(Math.random() * 10000000),
        text,
        done: false,
        color: colors[colorIndex],
      }
      props.onSave(newData)
    }
  }

  return (
    <Modal visible={props.isVisible} style={styles.modal} 
        animationType="slide" 
        transparent={true}
        >
      <KeyboardAvoidingView style={styles.container} >
      <Text style={styles.title}>{title}</Text>

        <View style={styles.content}>
          <Text style={styles.label}>ToDo Text:</Text>
          <TextInput
            style={styles.input}
            onChangeText={setText}
            value={props.data?.text}
            multiline={true}
            numberOfLines={10}
            // keyboardType="numeric"
          />

          <Text style={styles.label}>ToDo Color:</Text>
          <View style={styles.colors} >
            <View style={[styles.color, {
              backgroundColor: colors[0],
              borderColor: 'black',
              borderWidth: colorIndex === 0 ? 4 : 0
            }]} 
              >
                 <TouchableOpacity 
                  style={{height: '100%', width:'100%'}}
                  onPress={() => setColorIndex(0)}>
                </TouchableOpacity>
              </View>
            <View style={[styles.color, {
              backgroundColor: colors[1],
              borderColor: 'black',
              borderWidth: colorIndex === 1 ? 4 : 0}]} 
              >
                <TouchableOpacity 
                  style={{height: '100%', width:'100%'}}
                  onPress={() => setColorIndex(1)}>
                </TouchableOpacity>
              </View>
            <View style={[styles.color, {
              backgroundColor: colors[2],
              borderColor: 'black',
              borderWidth: colorIndex === 2 ? 4 : 0}]} 
            >
                 <TouchableOpacity 
                  style={{height: '100%', width:'100%'}}
                  onPress={() => setColorIndex(2)}>
                </TouchableOpacity>
              </View>
          </View>
        </View>

        <View style={styles.menu} >
          <TouchableOpacity
                  style={styles.button}
                  onPress={() => props.onClose()}
                >
                  <Text style={styles.buttonText}>Cancel</Text>
          </TouchableOpacity>
          <TouchableOpacity
                  style={styles.button}
                  onPress={onSave}
                >
                  <Text style={styles.buttonText}>Save</Text>
          </TouchableOpacity>
        </View>
      </KeyboardAvoidingView>
    </Modal>
  )
}

export default EditTodoView


const styles = StyleSheet.create({
  modal: {
    backgroundColor: 'rgba(0,0,0,0)',
  },
  container: {
    width: '100%', 
    height: '100%',
    paddingTop: 100,
    // backgroundColor: '#fff',
    backgroundColor: 'rgba(0,0,0,0.7)',
    flexDirection: 'column',
  },
  content: {
    backgroundColor: '#fff',
    flexDirection: 'column',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    padding: 20,
    paddingBottom: 0,
    backgroundColor: '#fff',
  },
  menu: {
    display: 'flex',
    width: '100%', height: 60,
    paddingLeft: 30,
    paddingTop: 15,
    flexDirection: 'row',
    justifyContent: 'space-between',
    backgroundColor: '#fff'
  },
  input: {
    height: 'auto',
    margin: 12,
    borderWidth: 1,
    padding: 10,
  },
  label: {
    padding: 10,
    paddingBottom: 0,
  },
  colors: {
    width: '100%',
    display: 'flex',
    flexDirection: 'row',
    padding: 20,
  },
  color: {
    width: 30, height: 30,
    marginRight: 20,
    borderRadius: 3,
  },
  button: {
    height: 20,
    width: 100,
  },
  buttonText: {
    fontSize: 18,
    color: '#007fff'
  }
});

Add Todo Modal Dialog

Main View

Update the file App.tsx with:

import React, { useState } from 'react';
import { StyleSheet, Text, SafeAreaView } from 'react-native';
import Menu from './components/menu';
import TodoList from './components/todo-list';
import ITodo from './models/todo.model';
import EditTodoView from './views/edit-todo.view';

export default function App() {

  const [data, setData] = useState<ITodo[]>([])
  const [isEditTodoVisible, setIsEditTodoVisible] = useState(false)

  const onAddTodo = () => {   
    setIsEditTodoVisible(true)
  }

  const onCloseEditTodo = () => {
    setIsEditTodoVisible(false)
  }

  const onSaveTodo = (data: ITodo) => {
    setData((d) => [...d, data])
    setIsEditTodoVisible(false)
  }

  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.title}>ToDo</Text>
      <TodoList data={data} />
      <Menu onAddTodo={onAddTodo}/>

      <EditTodoView isVisible={isEditTodoVisible} 
        onClose={onCloseEditTodo}
        onSave={onSaveTodo}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    display: 'flex',
    flexDirection: 'column',
    backgroundColor: '#fff',
    height: '100%',
    width: '100%',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    padding: 20,
    paddingBottom: 0,
  },
});

Main View

You can find the source code here