by Francesco Agnoletto
React Native Turotial: Todo App
With TypeScript!
January 13, 2020The 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.