DEV Community

Cover image for How to Build a TODO List with React Hooks
Amanda Fawcett for Educative

Posted on • Originally published at yazeedb.com

How to Build a TODO List with React Hooks

This article was originally posted on Educative. It was written by Yazeed Bzadough, who creates motivational and educational content for developers, hoping to inspire and teach with a deep understanding. He mainly focuses on web technologies–currently JavaScript, TypeScript, and React.


What are hooks?

They’re functions that give you React features like state and lifecycle hooks without ES6 classes.

Some benefits are:

  • Isolating stateful logic, making it easier to test.
  • Sharing stateful logic without render props or higher-order components.
  • Separating your app’s concerns based on logic, not lifecycle hooks.
  • Avoiding ES6 classes, because they’re quirky, not actually classes, and trip up even experienced JavaScript developers.

For more detail see React’s official Hooks intro.

Don’t Use in Production! At the time of this writing, Hooks are in alpha. Their API can change at any time. I recommend you experiment, have fun, and use Hooks in your side projects, but not in production code until they’re stable.


Let’s build a TODO list

Alt Text

Todo lists are the most overused example for a good reason — they’re fantastic practice. I recommend this for any language or library you want to try out.

Ours will only do a few things:

  • Display todos in a nice Material Design fashion
  • Allow adding todos via input
  • Delete todos

Setup

Here are the GitHub and CodeSandbox links.

git clone https://github.com/yazeedb/react-hooks-todo
cd react-hooks-todo
npm install
Enter fullscreen mode Exit fullscreen mode

The master branch has the finished project, so check out the start branch if you wish to follow along.

git checkout start

And run the project.

npm start

The app should be running on localhost:3000, and here’s our initial UI.

Alt Text

It’s already set up with material-ui to give our page a professional look. Let’s start adding some functionality!

The TodoForm component

Add a new file, src/TodoForm.js. Here’s the starting code.

import React from 'react';
import TextField from '@material-ui/core/TextField';

const TodoForm = ({ saveTodo }) => {
  return (
    <form>
      <TextField variant="outlined" placeholder="Add todo" margin="normal" />
    </form>
  );
};

export default TodoForm;
Enter fullscreen mode Exit fullscreen mode

Given the name, we know its job is to add todos to our state. Speaking of which, here’s our first hook.


useState

Check this code out:

import { useState } from 'react';

const [value, setValue] = useState('');
Enter fullscreen mode Exit fullscreen mode

useState is just a function that takes the initial state and returns an array. Go ahead and console.log it.

The array’s first index is your state’s current value, and the second index is an updater function.

So we appropriately named them value and setValue using ES6 destructuring assignment.


useState with forms

Our form should track the input’s value and call saveTodo upon submit. useState can help us with that!

Update TodoForm.js, the new code’s in bold.

import React, { useState } from 'react';
import TextField from '@material-ui/core/TextField';

const TodoForm = ({ saveTodo }) => {
  const [value, setValue] = useState('');

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        saveTodo(value);
      }}
    >
      <TextField
        variant="outlined"
        placeholder="Add todo"
        margin="normal"
        onChange={(event) => {
          setValue(event.target.value);
        }}
        value={value}
      />
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Back in index.js, import and use this component.

// ...

import TodoForm from './TodoForm';

// ...

const App = () => {
  return (
    <div className="App">
      <Typography component="h1" variant="h2">
        Todos
      </Typography>

      <TodoForm saveTodo={console.warn} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now your value’s logged on submit (press enter).

Alt Text


useState with Todos

We also need state for our todos. Import useState in index.js. Our initial state should be an empty array.

import React, { useState } from 'react';

// ...

const App = () => {
  const [todos, setTodos] = useState([]);

  // ...
};
Enter fullscreen mode Exit fullscreen mode

TodoList component

Create a new file called src/TodoList.js. Edit: Thank you Takahiro Hata for helping me move onClick to the correct spot!

import React from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import ListItemText from '@material-ui/core/ListItemText';
import Checkbox from '@material-ui/core/Checkbox';
import IconButton from '@material-ui/core/IconButton';
import DeleteIcon from '@material-ui/icons/Delete';

const TodoList = ({ todos, deleteTodo }) => (
  <List>
    {todos.map((todo, index) => (
      <ListItem key={index.toString()} dense button>
        <Checkbox tabIndex={-1} disableRipple />
        <ListItemText primary={todo} />
        <ListItemSecondaryAction>
          <IconButton
            aria-label="Delete"
            onClick={() => {
              deleteTodo(index);
            }}
          >
            <DeleteIcon />
          </IconButton>
        </ListItemSecondaryAction>
      </ListItem>
    ))}
  </List>
);

export default TodoList;
Enter fullscreen mode Exit fullscreen mode

It takes two props

  • Todos: The array of todos. We map over each one and create a list item.
  • DeleteTodo: Clicking a todo’s IconButton fires this function. It passes the index, which will uniquely identify a todo in our list.

Import this component in your index.js.

import TodoList from './TodoList';
import './styles.css';

const App = () => {
  //...
};
Enter fullscreen mode Exit fullscreen mode

And use it in your App function like so:

<TodoForm saveTodo={console.warn} />
<TodoList todos={todos} />
Enter fullscreen mode Exit fullscreen mode

Adding Todos

Still in index.js, let’s edit our TodoForm’s prop, saveTodo.

<TodoForm
  saveTodo={(todoText) => {
    const trimmedText = todoText.trim();

    if (trimmedText.length > 0) {
      setTodos([...todos, trimmedText]);
    }
  }}
/>
Enter fullscreen mode Exit fullscreen mode

Simply merge the existing todos with our new one, extra whitespace cut out.

We can add todos now!

Alt Text


Clearing the Input

Notice the input isn’t clearing after adding a new todo. That’s a bad user experience!

We can fix it with a small code change in TodoForm.js.

<form
  onSubmit={(event) => {
    event.preventDefault();

    saveTodo(value);

    setValue('');
  }}
/>
Enter fullscreen mode Exit fullscreen mode

Once a todo’s saved, set the form state to an empty string.

It’s looking good now!

Alt Text


Deleting Todos

TodoList provides each todo’s index, as it’s a guaranteed way to find the one we want to delete.

TodoList.js

<IconButton
  aria-label="Delete"
  onClick={() => {
    deleteTodo(index);
  }}
>
  <DeleteIcon />
</IconButton>
Enter fullscreen mode Exit fullscreen mode

We’ll take advantage of that in index.js.

<TodoList
  todos={todos}
  deleteTodo={(todoIndex) => {
    const newTodos = todos.filter((_, index) => index !== todoIndex);

    setTodos(newTodos);
  }}
/>
Enter fullscreen mode Exit fullscreen mode

Whatever todos don’t match the provided index are kept and stored in state using setTodos.

Delete functionality is complete!

Alt Text


Abstracting Todos useState

I mentioned that Hooks are great for separating state and component logic. Here’s what that may look like in our todo app.

Create a new file called src/useTodoState.js.

import { useState } from 'react';

export default (initialValue) => {
  const [todos, setTodos] = useState(initialValue);

  return {
    todos,
    addTodo: (todoText) => {
      setTodos([...todos, todoText]);
    },
    deleteTodo: (todoIndex) => {
      const newTodos = todos.filter((_, index) => index !== todoIndex);

      setTodos(newTodos);
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

It’s our same code from index.js, but separated! Our state management’s no longer tightly coupled to the component.

Now just import it.

import React from 'react';
import ReactDOM from 'react-dom';
import Typography from '@material-ui/core/Typography';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
import useTodoState from './useTodoState';
import './styles.css';

const App = () => {
  const { todos, addTodo, deleteTodo } = useTodoState([]);

  return (
    <div className="App">
      <Typography component="h1" variant="h2">
        Todos
      </Typography>

      <TodoForm
        saveTodo={(todoText) => {
          const trimmedText = todoText.trim();

          if (trimmedText.length > 0) {
            addTodo(trimmedText);
          }
        }}
      />

      <TodoList todos={todos} deleteTodo={deleteTodo} />
    </div>
  );
};

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
Enter fullscreen mode Exit fullscreen mode

And everything still works like normal.


Abstracting form input useState

We can do the same with our form!

Create a new file, src/useInputState.js.

import { useState } from 'react';

export default (initialValue) => {
  const [value, setValue] = useState(initialValue);

  return {
    value,
    onChange: (event) => {
      setValue(event.target.value);
    },
    reset: () => setValue('')
  };
};
Enter fullscreen mode Exit fullscreen mode

And now TodoForm.js should look like this.

import React from 'react';
import TextField from '@material-ui/core/TextField';
import useInputState from './useInputState';

const TodoForm = ({ saveTodo }) => {
  const { value, reset, onChange } = useInputState('');

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();

        saveTodo(value);
        reset();
      }}
    >
      <TextField
        variant="outlined"
        placeholder="Add todo"
        margin="normal"
        onChange={onChange}
        value={value}
      />
    </form>
  );
};

export default TodoForm;
Enter fullscreen mode Exit fullscreen mode

And we’re all done! Hope you enjoyed, until next time!

If you’d like more information on working with hooks, you can visit Advanced React Patterns with Hooks. Also, if you’d like to see more of Yazeed’s work, you can check out his course Functional Programming Patterns with RamdaJS.

Top comments (1)

Collapse
 
madza profile image
Madza

I wonder how much time across the world is being spent on rewriting all the class components when refactoring projects that are react < 16.8.0 xddd