Todo List

Develop a simple todo list where users can add items to a list using a text input and a button. Items should also be removable.

Solution

Click an item to remove it

  • Enter a new task in the todo list
  • Click a task to remove it
  • Try adding a blank entry

Explanation

useState vs useRef

To keep track of the task list, you need to use the useState hook. To access the users input you could use state but as it is just a reference to be added to your task list, useRef will be more efficient.

Semantic HTML for accessibility

Notice that the tasks are rendered to the page inside an unordered list. For accessibility reasons, it's makes sense to add semantic HTML.

How to add a new task

Adding a new task takes the input via the inputData useRef and runs it through handleSubmit. After checking the data a new unique id is added. Note how we update the todos state with a function and copy the previous state before resetting the input.

Think of the inverse approach to delete

The deleteHandler works by filtering all values that are not equal to the task you want to remove.

Code

import { useRef, useState } from "react";
import styles from "./Solution.module.css";
import { v4 as uuid } from "uuid";

type todoProps = {
  id: string;
  todo: string;
};

type todosProps = todoProps[];

const Solution = () => {
  const inputData = useRef<HTMLInputElement>(null);
  const [todos, setTodos] = useState<todosProps | []>([
    { id: "1", todo: "Enter a new task in the todo list" },
    { id: "2", todo: "Click a task to remove it" },
    { id: "3", todo: "Try adding a blank entry" },
  ]);
  const [error, setError] = useState(false);

  function handleSubmit(): void {
    const text: string | undefined = inputData?.current?.value.trim();
    if (!text) {
      setError(true);
      return;
    }
    setError(false);
    const task: todoProps = {
      id: uuid(),
      todo: text,
    };

    setTodos((prev) => [...prev, task]);

    if (inputData.current) {
      inputData.current.value = "";
    }
  }

  function deleteHandler(id: string): void {
    const newTodos: todosProps = todos.filter((item) => item.id !== id);
    setTodos(newTodos);
  }

  return (
    <div className={styles.container}>
      <p>Click an item to remove it</p>
      <ul className={styles.list}>
        {todos &&
          todos.map((todo) => {
            return (
              <li
                key={todo.id}
                className={styles.listItem}
                onClick={() => deleteHandler(todo.id)}
              >
                {todo.todo}
              </li>
            );
          })}
      </ul>
      <div className={styles.inputContainer}>
        <input
          className={styles.input}
          type="text"
          name="input"
          id="input"
          ref={inputData}
        />
        <button onClick={handleSubmit} className={styles.button}>
          Add
        </button>
      </div>
      {error && <p className={styles.error}>Enter a value</p>}
    </div>
  );
};

export default Solution;

Styling

.container {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  
  .inputContainer {
    display: flex;
    gap: 10px;
  }
  
  .input {
    padding: 5px;
    width: 400px;
    border: 1px solid rgb(189, 189, 189);
  }
  
  .button {
    padding: 10px 25px;
    background-color: var(--white);
    border: 1px solid var(--code-background);
    cursor: pointer;
    transition: filter 0.3s;
  }
  
  .button:hover {
    filter: brightness(0.8);
  }
  
  .input,
  .button {
    border-radius: 5px;
  }
  
  .title {
    margin: 10px 0;
  }
  
  .list {
    display: flex;
    flex-direction: column;
    gap: 15px;
    margin: 10px 0 20px;
  }
  
  .listItem {
    border: 1px dotted grey;
    width: 400px;
    overflow-wrap: break-word;
    padding: 10px;
    transition: background-color 0.3s;
  }
  
  .listItem:hover,
  .listItem:active {
    background-color: var(--colour-primary);
    cursor: pointer;
  }
  
  .error {
    color: rgb(218, 56, 56);
  }
  

Links