Editable todo-list in React

In this tutorial, we will see how to create an editable to-do list in React.

Frontend programming interviews cannot be done without being asked to implement a to-do list, it is a classic problem that covers all the major features of React and shows cases that the candidate understands the React in and out very clearly.

We are going to implement the following features in the to-do list.

  • Todo’s will be typed in the input and box and will be added to the list when enter is pressed.
  • Each todo will be in an in-complete state and when clicked on done, will move to a completed state. Once completed strike out the text.
  • When double-clicking on the todo, we can edit the text.
  • Todo’s can be deleted.

Editable Todo list in React

Creating the structure of to-do list in React

Let us start implementing the to-do list.

The first thing we will do is create the list-item component that will display each to-do and handle all its actions.

const Item = ({ text, completed, id }) => {
  return (
    <div className="item">
      <div class="circle">{completed ? <span>&#10003;</span> : ""}</div>
      <div className={completed ? "strike" : ""}>text</div>
      <div class="close">X</div>
    </div>
  );
};

This can be reused in the parent component, in which we will have an input box, where all the todo’s will be typed and once enter is pressed, we will store the todo in a list in the state and empty the input box.

To empty the input box, we will create a reference to it using useRef and after the item is added to the state we will empty the input’s value.

function App() {
  const [todos, setTodos] = useState([]);
  const inputRef = useRef();

  const handleKeyPress = (e) => {
    if (e.key === "Enter") {
      setTodos([
        ...todos,
        { text: e.target.value, completed: false, id: Date.now() }
      ]);
      inputRef.current.value = "";
    }
  };

  return (
    <div className="App">
      <input type="text" onKeyPress={handleKeyPress} ref={inputRef} />
      {todos.map((e) => (
        <Item
          {...e}
          key={e.id}
        />
      ))}
    </div>
  );
}

We are using Date.now() as an id and iterating all the list items below the input box.

Handling the to-do actions

Now all we have to do is handle the actions, such as toggling the completed state, deleting the todo, and updating the text.

All these actions will be present in the parent component as this is where we are maintaining the state but will be triggered from the child component.

export default function App() {
  const [todos, setTodos] = useState([]);
  const inputRef = useRef();

  const handleKeyPress = (e) => {
    if (e.key === "Enter") {
      setTodos([
        ...todos,
        { text: e.target.value, completed: false, id: Date.now() }
      ]);
      inputRef.current.value = "";
    }
  };
 
  // toggle completed
  const handleCompleted = (id) => {
    const updatedList = todos.map((e) => {
      if (e.id === id) {
        e.completed = !e.completed;
      }

      return e;
    });

    setTodos(updatedList);
  };

  // delete item
  const handleDelete = (id) => {
    const filter = todos.filter((e) => e.id !== id);
    setTodos(filter);
  };

  // handle text update
  const handleUpdateText = (id, text) => {
    const updatedList = todos.map((e) => {
      if (e.id === id) {
        e.text = text;
      }

      return e;
    });

    setTodos(updatedList);
  };

  return (
    <div className="App">
      <input type="text" onKeyPress={handleKeyPress} ref={inputRef} />
      {todos.map((e) => (
        <Item
          {...e}
          key={e.id}
          updateCompleted={handleCompleted}
          deleteTodo={handleDelete}
          updateText={handleUpdateText}
        />
      ))}
    </div>
  );
}

In the item component, we will maintain the state to handle the double-click and show the input box to edit the text. As at any given time, only one item can be edited (it will hide if the input goes out of focus). We can safely have the state in the child component.

const Item = ({
  text,
  completed,
  id,
  updateCompleted,
  deleteTodo,
  updateText
}) => {
  const [edit, setEdit] = useState(false);
  const [editText, setEditText] = useState(text);

  return (
    <div className="item">
      <div class="circle" onClick={() => updateCompleted(id)}>
        {completed ? <span>&#10003;</span> : ""}
      </div>
      <div
        className={completed ? "strike" : ""}
        onDoubleClick={() => {
          if (!completed) {
            setEdit(true);
          }
        }}
      >
        {edit ? (
          <input
            type="text"
            value={editText}
            onChange={(e) => {
              setEditText(e.target.value);
            }}
            onBlur={() => {
              setEdit(false);
              updateText(id, editText);
            }}
          />
        ) : (
          text
        )}
      </div>
      <div class="close" onClick={() => deleteTodo(id)}>
        X
      </div>
    </div>
  );
};

This will render an editable to-do list.

Styling for the to-do list

.App {
  font-family: sans-serif;
  text-align: center;
  max-width: 300px;
  margin: 0 auto;
}

.item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 5px;
  border-bottom: 1px solid #000;
}

.close {
  cursor: pointer;
  opacity: 0;
  transition: all 0.2s ease;
}

.item:hover .close {
  opacity: 1;
}

.circle {
  width: 30px;
  height: 30px;
  border-radius: 50px;
  border: 1px solid green;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

input {
  width: 100%;
  padding: 5px;
  font: 1.2em;
  margin-bottom: 20px;
}

.strike {
  text-decoration: line-through;
}