Build a Simple Todo App with React Native and TypeScript by@alexadam

Build a Simple Todo App with React Native and TypeScript

How To Setup React Native and Create a Simple ToDo App
image
Alex Adam HackerNoon profile picture

Alex Adam

Creative Coder



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

Add Todo Modal Dialog

image


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,
  },
});

image

image


You can find the source code here

Also published here

react to story with heart
react to story with light
react to story with boat
react to story with money
L O A D I N G
. . . comments & more!