by Francesco Agnoletto

React Native Turotial: Todo App

With TypeScript!

Photo by Toa Heftiba Şinca from Pexels

The cousin of the “hello world” program, the todo app is a good way to get started in a new technology front-end wise.

Starting out

This tutorial uses “Set up React Native, TypeScript and ESLint + prettier” to set up a basic React Native project. This tutorial uses TypeScript but it can be replicated just as easily in JavaScript.

The coding part

We want our todo app to have two views: the default view where the todos are displayed, and the edit view where new todos can be written.

We will allow adding and removing todo items, but not modifying existing ones. All todos will have a name key as a unique id. More properties can be easily added afterward.

For our application state, we will use React Native’s AsyncStorage. Since our state is not complicated, (3 actions, list, save, delete) we will use React hooks to handle all logic.

We will not use any other library except for uuid to generate unique IDs.

Handling the views

App.tsx should handle our views. The easiest way to do so is with a ternary operator switching component to be rendered.

// App.tsx
import React from "react";
import { StyleSheet, View, Button } from "react-native";

import { DefaultView, EditView } from "./app/components";

export type viewState = "default" | "edit";

const App = () => {
  const [
    viewState,
    setViewState
  ] = React.useState<viewState>("default");

  const handleNewPress = () => setViewState("edit");
  const handleCancelPress = () => setViewState("default");

  return (
    <View style={styles.container}>
      {viewState === "default" ? (
        <>
          <DefaultView />
          <Button title="Add new Item" onPress={handleNewPress}>
            Add new Item
          </Button>
        </>
      ) : (
        <>
          <EditView setViewState={handleCancelPress} />
          <Button title="Cancel" onPress={handleCancelPress}>
            Cancel
          </Button>
        </>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

export default App;

With an useState hook, we can define our 2 views. A couple of buttons will be responsible for switching between the two views.

We know that EditView will need a submit button to add todos. The button will also have to switch view so we need to pass setViewState as a prop.

The DefaultView and EditView components are still missing, but we will take care of them later. Our next priority is the logic of the todo application.

Handling the logic

Relying on React Native’s AsyncStorage simplifies the amount of logic we have to handle. Everything we need can be reduced in three actions:

  • Add a new todo
  • Delete an existing todo
  • List all todos

We can consider our data as an array of objects (the todos).
Initially, we just display a name property.
Deleting can be messy if more todos share the same name, a unique ID solves the issue.

// store.ts
import { AsyncStorage } from "react-native";
import uuidv4 from "uuid/v4";

const ITEMS_KEY = "ITEMS";

export interface Item {
  id: string;
  name: string;
}

// add a new todo
export const addItem = async (itemName: string) => {
  const item: Item = { name: itemName, id: uuidv4() };
  try {
    const storedItems: Item[] = JSON.parse(
      await AsyncStorage.getItem(ITEMS_KEY)
    );
    // itemsArray will be null if there are no elements
    // we need to make it an array in that case
    const itemsArray = storedItems || [];
    await AsyncStorage.setItem(
      ITEMS_KEY,
      JSON.stringify([...itemsArray, item])
    );
  } catch (error) {
    console.error(error);
  }
};

// remove an existing todo
export const deleteItem = async (item: Item) => {
  try {
    const storedItems: Item[] = JSON.parse(
      await AsyncStorage.getItem(ITEMS_KEY)
    );
    const filteredItems = storedItems.filter(
      selectedItem => selectedItem.id !== item.id
    );
    await AsyncStorage.setItem(
      ITEMS_KEY,
      JSON.stringify(filteredItems)
    );
  } catch (error) {
    console.error(error);
  }
};

type getItemsType = () => Promise<Array<Item>>;

// list all todos
export const getItems: getItemsType = async () => {
  try {
    const itemsArray: Item[] = JSON.parse(
      await AsyncStorage.getItem(ITEMS_KEY)
    );
    return itemsArray;
  } catch (error) {
    console.error(error);
  }
  return [];
};

Adding new todos

The EditView will use only the addItem action, as well as some React to handle the input. We will also need the uuid library to create a unique ID.

// EditView.tsx
import React from "react";
import { Text, TextInput, Button } from "react-native";

import { addItem } from "../store";
import { viewState } from "../../App";

interface IProps {
  setViewState: React.Dispatch<React.SetStateAction<viewState>>;
}

const EditView: React.FC<IProps> = ({ setViewState }) => {
  const [
    itemName,
    setItemName
  ] = React.useState<string>("");

  const onSubmit = async () => {
    await addItem(itemName);
    setViewState("default");
  };

  return (
    <>
      <Text>Add a new Item</Text>
      <TextInput
        value={itemName}
        placeholder="Title"
        onChangeText={setItemName}
      />
      <Button title="Submit" onPress={onSubmit}>
        Submit
      </Button>
    </>
  );
};

export default EditView;

Inputs in React Native work pretty much the same as standard React, same for buttons. The only visible difference is the name of the change prop.

Displaying the list

The DefaultView will be responsible for showing all the todos using the getItems action.
We need to asynchronously load our todos after the component gets mounted. Since we are using functional components, we can achieve this using the useEffect hook. Another useState hook will hold the data returned from getItems.

// DefaultView.tsx
import React from "react";
import { Text, FlatList } from "react-native";

import { Item, getItems } from "../store";
import ListItem from "./ListItem";

const DefaultView: React.FC = () => {
  const [data, setData] = React.useState<Array<Item>>([]);

  React.useEffect(() => {
    // isMounted unsubscribes React from the async operation
    // if the component is unmounted
    let isMounted = true;
    getItems().then((items: Item[]) => {
      if (isMounted) setData(items);
    });
    return () => (isMounted = false);
  }, [data]);

  return (
    <>
      <Text>Todo List</Text>
      <FlatList
        data={data}
        renderItem={({ item }) => <ListItem item={item} />}
        keyExtractor={item => item.id}
      />
    </>
  );
};

export default DefaultView;

The individual todos are wrapped in a ListItem component. ListItem will render the data as well and handle the deleteItem action.

List todos and deleting

By extracting all the todos rendering to a single component, we can easily expand the todo app by touching the least amount of files and code.

// ListItem.tsx
import React from "react";
import { Text, Button } from "react-native";

import { Item, deleteItem } from "../store";

interface IProps {
  item: Item;
}

const ListItem: React.FC<IProps> = ({ item }) => (
  <>
    <Text>{item.name}</Text>
    <Button title="Delete" onPress={() => deleteItem(item)}>
      Delete
    </Button>
  </>
);

export default ListItem;

Adding more properties to our todos will just need minimal changes to the addTodo method and Item interface in store.ts. As for rendering, ListItem.tsx is the only file that should be touched.
This assures that future refactors and new features will be small in scope and easy to add.

The full code of this project can be found here.