Search code examples
reactjs

React Form Input Value Does Not Update Passed One Value onChange


I have a very basic react page to update a data structure: list and send the update to a database. Some context for the snippet below: a list is just a basic structure that contains 2 values and an array. The list will either be empty or it will be initialize with props if there is a list passed to it. The list gets initialized properly and I can see all of the data in the console.

If there is data in the list, the input fields in the component will be defaulted to those values. The title and owner inputs work as intended and I have no problems with them. The fields that I have problems with are the list items fields.

I have to create the list item fields dynamically, because I do not know at any given time how many list items are going to be in the list. The number of inputs for the list items are generated correctly and the data in the inputs is correct. The problem is that when I try to change the value of the inputs for list items, I can only ever update up to one character at the end of the string.

I can see the value being updated in the console, but the value in the input does not change unless, very strangely, I go up and change a value in title or owner.

For example, if I have an input that looks like this:

item 1

and I try to update it to this:

item 1 with more info

the input will only append 1 character to the field and it will end up like this:

item 1o

where the last 'o' is the 'o' in 'info'

I hope that makes sense.

I suspect it has something to do with the fact that I cannot invoke setListItems without passing in the whole new listItems array. I can't update the hooked item individually, or at least I don't know how and all of my research comes to nothing.

const EditList = (props) => {
    let list = {
        title: '',
        owner: '',
        listItems: [],
    };

    let newListCreation = true;

    const [listTitle, setListTitle] = useState('');
    const [owner, setOwner] = useState('');
    const [listItems, setListItems] = useState([]);
    const navigate = useNavigate();

    if (props.list) {
        list = props.list;
        newListCreation = false;

        console.log(list);
    };

    let refresh = false;

    useEffect(() => {
        setListTitle(list.title);
        setOwner(list.owner);
        setListItems(list.listItems);
    }, [refresh]);

    const handleFormSubmit = (event) => {
        event.preventDefault();

        console.log("submit");
        const editedList = {
            id: list.id,
            title: listTitle,
            owner: owner,
            listItems: listItems,
        };

        saveList(editedList);
    };

    const saveList = async (list) => {
        setListItems(list.listItems);

        let response;

        console.log(list);

        if (newListCreation)
            response = await dataSource.post('/list', list);
        else
            response = await dataSource.put('/list', list);
        console.log(response);
        console.log(response.data);
        props.onEditList(navigate);
    };

    const handleCancel = () => {
        navigate("/");
    };

    const updateOwner = (event) => {
        setOwner(event.target.value);
    };

    const updateTitle = (event) => {
        setListTitle(event.target.value);
    };

    const updateListItem = (event) => {
        console.log("list items: ", listItems);
        console.log("event log id: ", event.target.id);
        console.log(list.listItems[0].itemText);
        console.log(event.target.value);

        for (let i = 0; i < list.listItems.length; i++) {
            if (parseInt(list.listItems[i].id) === parseInt(event.target.id)) {
                list.listItems[i].itemText = event.target.value;
            }
        }
    }

    const renderItemList = list.listItems.map((listItem) => {
        return (
            <input key={listItem.id} type="text" id={listItem.id} placeholder="Enter list items here"
                className="form-control" value={listItem.itemText} onChange={updateListItem} />
        );
    });

    return (
        <div className="container-md">
            <form onSubmit={handleFormSubmit}>
                <h1>{newListCreation ? "Create New" : "Edit"}List </h1>
                <div className="mb-3">
                    <label className="form-label" htmlFor="listTitle">List Title</label>
                    <input type="text" id="listTitle" className="form-control" value={listTitle} onChange={updateTitle} />

                    <label className="form-label" htmlFor="listOwner">Owner</label>
                    <input type="text" id="listOwner" placeholder="Enter List Owner" className="form-control" value={owner} onChange={updateOwner} />

                    <label className="form-label" htmlFor="listItems">List Items</label>
                    {renderItemList}

                    <button type="button" className="btn btn-secondary">+ Add new list item +</button>
                </div>
                <div>
                    <button type="button" className="btn btn-light" onClick={handleCancel}>Cancel</button>
                    <button type="submit" className="btn btn-primary">Submit</button>
                </div>
            </form>
        </div>
    );
};

export default EditList;

I tried the above and also I tried to change the structure being pointed to by renderItemList to the listItems in the useState. Nothing will work.


Solution

  • If I understood everything correctly I fixed the problem:

    https://stackblitz.com/edit/vitejs-vite-x662kd

    Let me explain the code I did!

    First I created an ListItemInput component which is in its own file. I did it because it can then dependendly manage its own states:

       import { useState } from 'react';
    
       export const ListItemInput = ({ id, text, listItems, setListItems }) => {
        const [inputText, setInputText] = useState(text ?? '');
    
        const handleOnChange = (event) => {
        console.log(listItems);
        console.log('event log id: ', event.target.id);
    
        // Find the list item with the matching ID
        const updatedListItems = listItems.map((item) => {
          if (item.id === event.target.id) {
            setInputText(event.target.value);
            return { ...item, itemText: event.target.value };
          } else {
            return item;
          }
        });
    
        // Update the state with the modified list
        setListItems(updatedListItems);
        };
    
       return (
              <input
               key={id}
               type="text"
               id={id}
               placeholder="Enter list items here"
               className="form-control"
               value={inputText}
               onChange={(event) => handleOnChange(event)}
              />
    
           );
        };
    

    The input field has its own state, which sets it default value from the listItems state. The handleOnChange function is called everytime the input field value changes. It searches inside the listItems for its Id and then sets it value there.

    Then I initialised the Input fields depending on the array inside the list state.

      {list.listItems.map((listItem) => {
        return (
          <ListItemInput
            id={listItem.id}
            text={listItem.itemText}
            listItems={listItems}
            setListItems={setListItems}
          />
        );
      })}
    

    If you want an empty listItemInput field, I would recommend adding a button, which adds an Object to the listItems array, which stores an id and empty text.

    I think this should solve the problem, let me know if there are any questions!