Search code examples
javascriptreactjsuse-state

Handle dynamically created text inputs with usestate - ReactJS


I'm making a todo list in react js. Each time a new todo item is created, some buttons are appended next to it along with a edit input text box. I'm trying to avoid using refs but purely usestate for my case, however I can't figure out how to do it. At its current state, all edit text inputs are using the same state and that brings focus loss along with other issues. I'd highly appreciate any suggetsions.

import "./theme.css"

import * as appStyles from "./styles/App.module.css"
import * as todoStyles from "./styles/Todo.module.css"

import { useState } from "react"

const initialState = [
  {
    id: "1",
    name: "My first ToDo",
    status: "new",
  },
]

export function App() {
  const [numofItems, setNumofItems] = useState(2)
  const [newToDo, setnewToDo] = useState('');
  const [todos, setTodos] = useState(initialState);
  const [editTodo, setEditTodo] = useState({name: ""});

  const onAddTodo = () => {
    setnewToDo("");
    setTodos((old) => [
      ...old,
      { id: numofItems.toString(), name: newToDo, status: "new" },
    ])
    setNumofItems(numofItems + 1);
  }

   deleteList = () =>{
     setTodos([]);
  }

  const handleEdit = (id, description) =>{
    let el = todos.map((item) => {if(item.id === id) {item.name = description} return item});
    setTodos(el);
    setEditTodo('');
  }

  const handleMove = (id, position) =>{
    const search = obj => obj.id === id;
    const todoIndex = todos.findIndex(search);
    if(position === "up"){
      if (todos[todoIndex - 1] === undefined) {
      } else {
        const newTodo1 = [...todos];
        const temp1 = newTodo1[todoIndex - 1];
        const temp2 = newTodo1[todoIndex]
        newTodo1.splice(todoIndex - 1, 1, temp2);
        newTodo1.splice(todoIndex, 1, temp1);
        setTodos([...newTodo1]);
      }  
    }
    else if(position === "down"){
      if (todos[todoIndex + 1] === undefined) {
      } else {
        const newTodo1 = [...todos];
        const temp1 = newTodo1[todoIndex + 1];
        const temp2 = newTodo1[todoIndex]
        newTodo1.splice(todoIndex + 1, 1, temp2);
        newTodo1.splice(todoIndex, 1, temp1);
        setTodos([...newTodo1]);
      }  
    }
  }

  const Todo = ({ record }) => {
    return <li className={todoStyles.item}>{record.name}
              <button className={appStyles.editButtons} onClick={() => deleteListItem(record.id)} >Delete</button>
              <button className={appStyles.editButtons} onClick={() => handleEdit(record.id, editTodo.name)}>Edit</button>
              <button className={appStyles.editButtons} onClick={() => handleMove(record.id, "down")}>Move down</button>
              <button className={appStyles.editButtons} onClick={() => handleMove(record.id, "up")}>Move up</button>
              <input className={appStyles.input}
                  type = "text"
                  name={`editTodo_${record.id}`}
                  value = {editTodo.name}
                  onChange={event => {event.persist();
                    setEditTodo({name: event.target.value});}}
                /></li>
  }

  const deleteListItem = (todoid) => {
    setTodos(todos.filter(({id}) => id !== todoid))
  }

  return (
    <>
      <h3 className={appStyles.title}>React ToDo App</h3>
      <ul className={appStyles.list}>
        {todos.map((t, idx) => (
          <Todo key={`todo_${idx}`} record={t} />
        ))}
      </ul>
      <div className={appStyles.actions}>
        <form>
          <label>
            Enter new item:
            <input className={appStyles.input} type="text" name="newToDo" value={newToDo} onChange={event => setnewToDo(event.target.value)}/>
          </label>
        </form>
        <button
          className={appStyles.button}
          onClick={onAddTodo}
        >
          Add
        </button>
        <br></br>
        <button className={appStyles.button} onClick={this.deleteList}>
          Delete List
        </button>
      </div>
    </>
  )
}

Solution

  • Never define components in the body of another component. It will result in unmount/mount of that element every time it's rendered.

    Here is how you can split up the Todo component from you App:

    const Todo = ({ record, onDelete, onEdit, onMove }) => {
      const [inputValue, setInputValue] = useState(record.name);
      return (
        <li className={todoStyles.item}>
          {record.name}
          <button className={appStyles.editButtons} onClick={() => onDelete()}>
            Delete
          </button>
          <button
            className={appStyles.editButtons}
            onClick={() => onEdit(inputValue)}
          >
            Edit
          </button>
          <button className={appStyles.editButtons} onClick={() => onMove("down")}>
            Move down
          </button>
          <button className={appStyles.editButtons} onClick={() => onMove("up")}>
            Move up
          </button>
          <input
            className={appStyles.input}
            type="text"
            value={inputValue}
            onChange={(event) => {
              setInputValue(event.target.value);
            }}
          />
        </li>
      );
    };
    
    function App() {
      return (
        <>
          <ul className={appStyles.list}>
            {todos.map((t, idx) => (
              <Todo
                key={`todo_${idx}`}
                record={t}
                onDelete={() => deleteListItem(t.id)}
                onEdit={(description) => handleEdit(t.id, description)}
                onMove={(position) => handleMove(t.id, position)}
              />
            ))}
          </ul>
        </>
      );
    }
    
    

    Note: I've shown only the interesting bits, not your entire code.