Search code examples
reactjsjsxcontrolled-componenttodoist

Despite using 'onChange', why my input looses focus after any key press?


I've just started learning React and I'm trying to build a todo list including edit button. I defined an input inside

  • tags through a condition in component. but when i click on edit icon to rewrite a task, with the first keypress the focus is gone!

    import "./index.css";
    import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
    import { faDeleteLeft, faPenToSquare } from "@fortawesome/free-solid-svg-icons";
    import { useState } from "react";
    const todoItems = [
      { description: "Task 1", id: 111, done: false, edit: false },
      { description: "Task 2", id: 222, done: false, edit: false },
      { description: "Task 3", id: 333, done: false, edit: false },
    ];
    export default function App() {
      return (
        <div
          className="
        app"
        >
          <ToDo />
        </div>
      );
    }
    
    function ToDo() {
      const [tasks, setTasks] = useState(todoItems);
    
      function handleAddTask(task) {
        setTasks((tasks) => [...tasks, task]);
      }
    
      function handleToggleTask(id) {
        setTasks(
          tasks.map((task) =>
            task.id === id ? { ...task, done: !task.done } : task
          )
        );
      }
    
      function handleDeleteTask(id) {
        setTasks(tasks.filter((task) => task.id !== id));
      }
    
      function handleEditTask(id) {
        setTasks(
          tasks.map((task) =>
            task.id === id ? { ...task, edit: !task.edit } : task
          )
        );
      }
    
      const handleSaveEdit = (id, newText) => {
        setTasks(
          tasks.map((task) =>
            task.id === id ? { ...task, description: newText, edit: false } : task
          )
        );
      };
    
      return (
        <div className="todo-container">
          <Header />
          <TodoList
            tasks={tasks}
            onToggleTask={handleToggleTask}
            onDeleteTask={handleDeleteTask}
            onEditTask={handleEditTask}
            onSaveEdit={handleSaveEdit}
          />
          <TodoForm onAddTask={handleAddTask} />
        </div>
      );
    }
    
    function Header() {
      return (
        <header className="todo-header">
          <h2>Todo List</h2>
        </header>
      );
    }
    
    function TodoList({
      tasks,
      onToggleTask,
      onDeleteTask,
      onEditTask,
      onSaveEdit,
    }) {
      return (
        <ul className="todo-list">
          {tasks.map((task) => (
            <TodoItem
              task={task}
              onToggleTask={onToggleTask}
              onDeleteTask={onDeleteTask}
              onEditTask={onEditTask}
              onSaveEdit={onSaveEdit}
            />
          ))}
        </ul>
      );
    }
    
    function TodoItem({
      task,
      onToggleTask,
      onDeleteTask,
      onEditTask,
      onSaveEdit,
    }) {
      return (
        <li className="todo-item" key={task.id}>
          <input
            type="checkbox"
            value={task.done}
            onChange={() => onToggleTask(task.id)}
          />
          {task.edit ? (
            <input
              type="text"
              value={task.description}
              onChange={(e) => onSaveEdit(task.id, e.target.value)}
              style={{ width: "100%", border: "none", outline: "none" }}
            />
          ) : (
            <label className={task.done ? "done-task" : ""}>
              {task.description}
            </label>
          )}
          {!task.edit && (
            <>
              <FontAwesomeIcon
                icon={faPenToSquare}
                style={{ cursor: "pointer", margin: "0 10px" }}
                onClick={() => onEditTask(task.id)}
              />
              <FontAwesomeIcon
                style={{ cursor: "pointer" }}
                icon={faDeleteLeft}
                rotation={180}
                onClick={() => onDeleteTask(task.id)}
              />
            </>
          )}
        </li>
      );
    }
    
    function TodoForm({ onAddTask }) {
      const [description, setDescription] = useState("");
      function handleSubmit(e) {
        e.preventDefault();
        if (description.trim() === "") return;
    
        const id = crypto.randomUUID();
        const newTask = { description, id, done: false, edit: false };
        console.log(newTask);
        onAddTask(newTask);
    
        setDescription("");
      }
      return (
        <form className="todo-input" onSubmit={(e) => handleSubmit(e)}>
          <input
            type="text"
            placeholder="Add a new task"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
          />
          <button>Add</button>
        </form>
      );
    }
    
    

    I've searched but i could not find the same case as mine! Can anybody help me on this?


  • Solution

  • That's exactly what you do on this line:

    onChange={(e) => onSaveEdit(task.id, e.target.value)}
    

    Which calls this handler, setting edit: false:

      const handleSaveEdit = (id, newText) => {
        setTasks(
          tasks.map((task) =>
            task.id === id ? { ...task, description: newText, edit: false } : task
          )
        );
      };
    

    You have two options:

    • Use an uncontrolled input with a "save" button on the side. Once the user is done editing that value, they can click the "save" button, which calls handleSaveEdit. newText can be obtained by adding a ref to the <input>. Additionally, you might want to have a "cancel" / "discard change" button.

    • Keep using a controlled input and automatically "save" the value as it changes. In this case, you just need a different handler that updates the target task's description without changing its edit property:

      const handleEdit = (id, newText) => {
        setTasks(
          tasks.map((task) =>
            task.id === id ? { ...task, description: newText } : task
          )
        );
      };