This guide is the second part of React Todo List, where used React to develop a simple todo list. Continuing on, we will use a mock API to retrieve and update the list and add functions to add new items.
The application will now list todo items from a predefined list, allow a user to mark any of them as complete, and update the list appropriately. However, there is no persistence for the changes. So, if the user refreshes the browser, the list goes back to the original list from the JSON file.
We need an API that can be used to retrieve the current list of items and update that list. Building a real API is beyond the scope of this guide, so we will use a mock API that stores the data in local storage but will be developed using promises so that it could easily be replaced by calls to a 'real' API in the future.
This mock API will need two functions - one to get the list of items and one to update an item to be complete and return the updated list:
1const storageKey = "TODO_ITEMS";
2const delayMs = 1000;
3
4function getFromStorage() {
5 const fromStorage = localStorage.getItem(storageKey);
6 return fromStorage ? JSON.parse(fromStorage) : [];
7}
8
9function get() {
10 return new Promise(resolve => {
11 setTimeout(() => resolve(getFromStorage()), delayMs);
12 });
13}
14
15function complete(id) {
16 return new Promise(resolve => {
17 const items = getFromStorage();
18 const updatedItems = items.map(item =>
19 (item.id === id ? { ...item, complete: true } : item));
20 localStorage.setItem(storageKey, JSON.stringify(updatedItems));
21 setTimeout(() => resolve(updatedItems), delayMs);
22 });
23}
When accessing an API over a network, a library such as fetch or axios which return data in a promise would usually be used. So, these mock API functions wait for one second - to simulate network latency - and also return their data in a promise. The get
function returns the list of items at the local storage key or, if there's nothing there, an empty list. The complete
function gets the list of items, updates them in the same way as the completeItem
function that was developed earlier, writes the updated list to local storage, and returns it in the promise.
The import of the original json file can now be removed and the items state can be initialized with an empty array, like this:
1const [items, setItems] = React.useState([]);
Rather than get the initial list of items from the json file, the application should now use the API get
function. To do this, a function to call the API, wait for the data and then set the state is needed:
1async function loadItems () {
2 const todoItems = await todoApi.get();
3 setItems(todoItems);
4}
This function uses the await operator to call the get
function and wait for the result; once the result is returned it updates the state. This function needs calling when the application starts and the way to do this is with the effect hook. When called with an empty array as dependencies, useEffect
will execute the effect when the component mounts, which is exactly what's needed, so this code can be added to our component to load the items:
1React.useEffect(() => { loadItems(); }, []);
Now, when the application starts, it will render nothing in the items for one second and then render the items in the list received from the API. We should give the user an indication that something is happening while we are getting the items from the API. This can be done by adding a loading state to the component that is set to true before calling get
and set to false when the data is returned. When it is set to true, render a loading message and when it is set to false, render the list of items.
This means adding a state and changing the loadItems
function, like this:
1const [loading, setLoading] = React.useState(false);
2async function loadItems () {
3 setLoading(true);
4 const todoItems = await todoApi.get();
5 setItems(todoItems);
6 setLoading(false);
7}
Also, completing an item should now call the API function, rather than just updating the state from within the component - the completeItem
function should be changed:
1async function completeItem (id) {
2 const todoItems = await todoApi.complete(id);
3 setItems(todoItems);
4}
Users are now able to click on an item to set it as complete and the list will update to show the item as complete.
The application is now reading the list of items from the mock API using local storage, meaning that right now it is returning no items in the list. Therefore, we need to be able to add a new item to the list.
Firstly, a new function is required in the mock API that will take a new item and return the new list. This function will follow the same pattern as the complete
API function and get the list out of local storage, update the list, write it back to local storage, and return it in a promise:
1function add(item) {
2 return new Promise(resolve => {
3 const items = getFromStorage();
4 const newId = items.reduce((id, item) => (item.id >= id ? item.id + 1 : id), 1);
5 const updatedItems = [...items, { ...item, id: newId }];
6 localStorage.setItem(storageKey, JSON.stringify(updatedItems));
7 setTimeout(() => resolve(updatedItems), delayMs);
8 });
9}
This API function can be called from the main application component in exactly the same way as the complete
function:
1async function add(item) {
2 const updatedItems = await todoApi.add(item);
3 setItems(updatedItems);
4}
Now the application needs a form to enter both the name of the item and the date on which it is due. This form will be a separate component that will accept two functions as props - one to call to add the item and one to call when cancelling the add function:
1function NewItem({ add, cancel }) {
2 const [name, setName] = React.useState("");
3 const [date, setDate] = React.useState(dateformat(new Date(), "yyyy-mm-dd"));
4
5 function addItem() {
6 const dueDate = new Date(date);
7 add({ name, timestampDue: dueDate.getTime(), complete: false, id: 0 });
8 }
9
10 return (
11 <div className="add-item-form">
12 <div className="form-group">
13 <label htmlFor="addItemInput">Item description</label>
14 <input
15 type="text"
16 placeholder="Enter description..."
17 className="form-control"
18 id="addItemInput"
19 value={name}
20 onChange={e => setName(e.target.value)}
21 />
22 </div>
23 <div className="form-group">
24 <label htmlFor="addItemDueInput">Due date</label>
25 <input
26 type="date"
27 className="form-control"
28 id="addItemDueInput"
29 onChange={e => setDate(e.target.value)}
30 value={date}
31 />
32 </div>
33 <button className="btn btn-success" disabled={name === ""} onClick={addItem}>
34 Add item
35 </button>
36 <button className="btn btn-secondary" style={{ marginLeft: "20px" }} onClick={cancel}>
37 Cancel
38 </button>
39 </div>);
40}
This NewItem
component contains two controlled input components, meaning that the two input components always set their value to the appropriate state - name
or date
- and, when changed, update the same state. The cancel button calls the cancel
prop when clicked and the add button calls the add
prop with an object created from the name
and date
states.
This component is to be displayed from within the Header
component, so a new prop of addItem
is added to the Header
- the add
function from the main application component will be passed as this prop.
State is required to control whether or not the NewItem
component is being displayed:
1const [adding, setAdding] = React.useState(false);
A new button is added that will only be shown when the adding
state is true
and, when clicked, will set adding
to true
:
1{!adding && (
2 <button type="button" className="btn btn-link" onClick={() => setAdding(true)}>
3 Add new item
4 </button>)}
Finally, the NewItem
component is displayed below the header when the adding
state is true
:
1{adding && <NewItem cancel={() => setAdding(false)} add={addNewItem} />}
The addItem
function, set as the add
prop, will set the adding
state to false and call the addItem
prop with the new item.
Now, when the 'Add item' button is clicked, the API will be updated with a new item and the updated list, including that item, will be rendered in the application.
In this two guide series, we have explored how to create a ToDo List in React. In the first section, React Todo List, we learned about how to display a list, add functions to mark an item as complete, and how to filter the list by complete and overdue items. We finished strong by learning how to use a mock API to retrieve and update the list and add functions to add new items.