Author avatar

Chris Dobson

React Todo List with Functions

Chris Dobson

  • Sep 6, 2019
  • 10 Min read
  • 279 Views
  • Sep 6, 2019
  • 10 Min read
  • 279 Views
Web Development
React

Introduction

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 guide requires a version of React that includes the hooks API and will assume that Bootstrap is available. Extra CSS will be provided by this file.

Use an API

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const storageKey = "TODO_ITEMS";
const delayMs = 1000;

function getFromStorage() {
  const fromStorage = localStorage.getItem(storageKey);
  return fromStorage ? JSON.parse(fromStorage) : [];
}

function get() {
  return new Promise(resolve => {
    setTimeout(() => resolve(getFromStorage()), delayMs);
  });
}

function complete(id) {
  return new Promise(resolve => {
    const items = getFromStorage();
    const updatedItems = items.map(item => 
      (item.id === id ? { ...item, complete: true } : item)); 
    localStorage.setItem(storageKey, JSON.stringify(updatedItems));
    setTimeout(() => resolve(updatedItems), delayMs);
  });
}
javascript

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:

1
const [items, setItems] = React.useState([]);
javascript

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:

1
2
3
4
async function loadItems () {
  const todoItems = await todoApi.get();
  setItems(todoItems);
}
javascript

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:

1
React.useEffect(() => { loadItems(); }, []);
javascript

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:

1
2
3
4
5
6
7
const [loading, setLoading] = React.useState(false);
async function loadItems () {
  setLoading(true);
  const todoItems = await todoApi.get();
  setItems(todoItems);
  setLoading(false);
}
javascript

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:

1
2
3
4
async function completeItem (id) {
  const todoItems = await todoApi.complete(id);
  setItems(todoItems);
}
javascript

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.

Add an Item

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:

1
2
3
4
5
6
7
8
9
function add(item) {
  return new Promise(resolve => {
    const items = getFromStorage();
    const newId = items.reduce((id, item) => (item.id >= id ? item.id + 1 : id), 1);
    const updatedItems = [...items, { ...item, id: newId }];
    localStorage.setItem(storageKey, JSON.stringify(updatedItems));
    setTimeout(() => resolve(updatedItems), delayMs);
  });
}
javascript

This API function can be called from the main application component in exactly the same way as the complete function:

1
2
3
4
async function add(item) {
  const updatedItems = await todoApi.add(item);
  setItems(updatedItems);
}
javascript

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function NewItem({ add, cancel }) {
  const [name, setName] = React.useState("");
  const [date, setDate] = React.useState(dateformat(new Date(), "yyyy-mm-dd"));

  function addItem() {
    const dueDate = new Date(date);
    add({ name, timestampDue: dueDate.getTime(), complete: false, id: 0 });
  }

  return (
    <div className="add-item-form">
      <div className="form-group">
        <label htmlFor="addItemInput">Item description</label>
        <input
          type="text"
          placeholder="Enter description..."
          className="form-control"
          id="addItemInput"
          value={name}
          onChange={e => setName(e.target.value)}
        />
      </div>
      <div className="form-group">
        <label htmlFor="addItemDueInput">Due date</label>
        <input
          type="date"
          className="form-control"
          id="addItemDueInput"
          onChange={e => setDate(e.target.value)}
          value={date}
        />
      </div>
      <button className="btn btn-success" disabled={name === ""} onClick={addItem}>
        Add item
      </button>
      <button className="btn btn-secondary" style={{ marginLeft: "20px" }} onClick={cancel}>
        Cancel
      </button>
    </div>);
}
javascript

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:

1
const [adding, setAdding] = React.useState(false);
javascript

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
2
3
4
{!adding && (
  <button type="button" className="btn btn-link" onClick={() => setAdding(true)}>
    Add new item
  </button>)}
javascript

Finally, the NewItem component is displayed below the header when the adding state is true:

1
{adding && <NewItem cancel={() => setAdding(false)} add={addNewItem} />}
javascript

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.

Conclusion

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.

5