1

I need to make changes in the "todo" element based on the "todoId" of the object. How can I do it? Like, if I want to change only the "todo" array of the object that has the "todoId" = 1, for example.

const [todoList, setTodoList] = useState([
    {
      todoId: 0,
      todoName: "Personal",
      todo: []
    },
    {
      todoId: 1,
      todoName: "Work",
      todo: []
    },
    {
      todoId: 2,
      todoName: "College",
      todo: []
    },
  ])
Lee Taylor
  • 7,761
  • 16
  • 33
  • 49
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find – DallogFheir Aug 02 '23 at 13:48
  • You may be interested in the [Choosing the State Structure tutorial on React's site](https://react.dev/learn/choosing-the-state-structure#avoid-deeply-nested-state) – Heretic Monkey Aug 02 '23 at 13:56

5 Answers5

0

You need to essentially recreate the whole array.

setTodoList((prev) => {
let updateObject= prev.filter((item) => item.todoId===1)
// do things to updateObject
return ([...prev.filter((item) => item.todoId!==1), updateObject])
})
Dean MacGregor
  • 11,847
  • 9
  • 34
  • 72
0

The updater function returned from the useState hook (setTodoList in your case) can be used with either of the following:

  • a value (for example, setTodoList([]) to set it to an empty array)
  • a callback that returns a value.

The callback takes a single prevState argument which is just the previous state value. The callback should then return a new state value.

This works great for array state variables since you will usually want to add/change/remove one item from the array.

Here's something you could try:

function changeItem (itemId, newItem) {
    setTodoList(function (oldTodoList) {
        // create a new array without the item where item.todoId == itemId
        const itemsWithoutNewId = oldTodoList.filter((item) => item.id !== id);
        // add the new item and this array together and put it in another array
        const itemsWithNew = [...itemsWithoutNewId, newItem];
        // Return the final array
        return itemsWithNew;
    });
}

or as a one-liner (functionally equivalent):

const changeItem = (id, newItem) => setTodoList(oldTodoList => [...oldTodoList.filter(item => item.todoId !== id), newItemForId]);

They can both be used like this:

const item = {
      todoId: 1,
      todoName: "New Todo",
      todo: []
};
changeItem(1, item);

console.log(todoList);
/*
[
    {
      todoId: 0,
      todoName: "Personal",
      todo: []
    },
    {
      todoId: 1,
      todoName: "New Todo",
      todo: []
    },
    {
      todoId: 2,
      todoName: "College",
      todo: []
    },
]
*/
R2509
  • 23
  • 7
0

You can create a function that receive the todoId and the todo data. and if finded the todo update the todo array.

const update = (id, data) => {
  setTodoList((prevTodos) => {
    const index = prevTodos.findIndex((todo) => todo.todoId === id);

    if (index !== -1) {
      prevTodos[index].todo = prevTodos[index].todo.concat(data);
    }

    return prevTodos;
  });

  console.log(todoList);
};
0

Here is an example of how you could add and remove items from the various todo lists.

You will need to use the function callback version when setting the state. Just clone the lists, find the one you want to add to (or remove from), and return the modified clone. When clicking on a button, the DOM is queried for data attributes to identify which list or item you are modifying.

Note: In the example below, I just set a timestamp when adding a new item. You could modify this to show a dialog where you could enter the text of the todo item.

Also, avoid using 0 for an ID. In most languages zero could mean: false, unknown, null, nil, unset, or undefined. Starting with 1 is preferred.

const { useCallback, useEffect, useState } = React;

const fetchTodos = () => Promise.resolve([
  { todoId: 1, todoName: "Personal", todo: [] },
  { todoId: 2, todoName: "Work",     todo: [] },
  { todoId: 3, todoName: "College",  todo: [] },
]);

const TodoItem = ({ text, handleRemove }) => (
  <div className="TodoItem">
    <span>{text}</span>
    <button type="button" onClick={handleRemove}>Remove</button>
  </div>
);

const TodoList = ({ uid, name, items, handleAdd, handleRemove }) => (
  <div className="TodoList">
    <div className="heading">
      <h2>{name}</h2>
      <button type="button" data-uid={uid} onClick={handleAdd}>Add</button>
    </div>
    <ul>
      {items.map(({ text }, index) => (
        <li key={text} data-index={index}>
          <TodoItem text={text} handleRemove={handleRemove} />
        </li>
      ))}
    </ul>
  </div>
);

const App = () => {
  const [todoLists, setTodoLists] = useState([]);
  
  useEffect(() => {
    fetchTodos().then(setTodoLists);
  }, []);
  
  const handleAdd = useCallback((e) => {
    const { uid } = e.target.dataset;
    setTodoLists((currentTodoLists) => {
      const copy = structuredClone(currentTodoLists);
      const found = copy.find(({ todoId }) => todoId === +uid);
      if (found) {
        found.todo.push({ text: Date.now() });
        return copy;
      }
      return currentTodoLists;
    });
  }, []);
  
  const handleRemove = useCallback((e) => {
    const { index } = e.target.closest('li').dataset;
    const { uid } = e.target.closest('.TodoList').querySelector('.heading button').dataset;
    setTodoLists((currentTodoLists) => {
      const copy = structuredClone(currentTodoLists);
      const found = copy.find(({ todoId }) => todoId === +uid);
      if (found) {
        found.todo.splice(+index, 1);
        return copy;
      }
      return currentTodoLists;
    });
    
  }, []);

  return (
    <div className="App">
      <h1>Todos</h1>
      {todoLists.map(({ todoId, todoName, todo }) => (
        <TodoList
          key={todoId}
          uid={todoId}
          name={todoName}
          items={todo}
          handleAdd={handleAdd}
          handleRemove={handleRemove}
        />
      ))}
    </div>
  );
};

ReactDOM
  .createRoot(document.getElementById("root"))
  .render(<App />);
h1, h2 { margin: 0; padding: 0; margin-bottom: 0.25rem; }

.TodoList ul {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;;
}

.TodoList .heading,
.TodoItem {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
0

Here's a expanded example based on your data. The key section is in handleClick. After the section has been selected from the dropdown, the todo entered in the input box, and the button clicked...

...we update the setTodoList state by taking the previous state and mapping over it. If the section name equals the value of the select state we spread the existing section object into a new object (no mutation!). We spread the section's todos array into a new array, adding in a new todo with an id (based on the current length of the todo array). We finally return the updated section object. If the section name and the select value don't match we return the section object without changes.

(Note, for this example, I've changed the property key names in the data to be a little more meaningful.)

const { useState } = React;

function Example({ initial }) {

  // States for the list, the select, and the input
  const [ todoList, setTodoList ] = useState(initial);
  const [ select, setSelect ] = useState('Choose section');
  const [ input, setInput ] = useState('');

  // Update the input state
  function handleInput(e) {
    setInput(e.target.value);
  }

  // When the button is clicked
  function handleClick() {
  
    // `map` over the previous state
    setTodoList(prev => {
      return prev.map(section => {
      
        // If the select value equals the current iterated
        // section name
        if (section.name === select) {

          // Spread the existing section in a new object
          // then spread out the existing section todos array
          // into a new array adding in a new todo object with its
          // own id
          return {
            ...section,
            todos: [
              ...section.todos,
              { id: section.todos.length + 1, text: input }
            ]
          };
        }
        
        // Otherwise just return the section
        return section;
      });
    });
  }

  // Update the select
  function handleSelect(e) {
    if (e.target.value !== 'Choose section') {
      setSelect(e.target.value);
    }
  }

  // Disabled the button if either the select or input values
  // are "empty"
  function isButtonDisabled() {
    return select === 'Choose section' || input === '';
  }

  return (
    <main>
      <section>
        <select value={select} onChange={handleSelect}>
          <option disabled>Choose section</option>
          {todoList.map(section => {
            return <Option key={section.id} option={section} />
          })}
        </select>
        <input type="text" onChange={handleInput} />
        <button
          type="button"
          disabled={isButtonDisabled()}
          onClick={handleClick}
        >Add todo
        </button>
      </section>
      <section>
        {todoList.map(section => {
          return (
            <TodoSection
              key={section.id}
              name={section.name}
              todos={section.todos}
            />
          );
        })}
      </section>
    </main>
  );

}

function Option({ option }) {
  return (
    <option
      value={option.name}
    >{option.name}
    </option>
  );
}

function TodoSection({ name, todos }) {
  return (
    <section className="section">
      <header>{name}</header>
      <ul>
        {todos.map(todo => {
          return <Todo key={todo.id} todo={todo} />
        })}
      </ul>
    </section>
  );
}

function Todo({ todo }) {
  return (
    <li>
      {todo.text}
    </li>
  );
}

const todoList=[{id:0,name:"Personal",todos:[]},{id:1,name:"Work",todos:[]},{id:2,name:"College",todos:[]}];

const node = document.getElementById('root');
const root = ReactDOM.createRoot(node);
root.render(<Example initial={todoList} />);
main > section ~ section { margin-top: 1rem; }
.section header { font-weight: 600; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.min.js"></script>
<div id="root"></div>
Andy
  • 61,948
  • 13
  • 68
  • 95