Deleting Specific Item From Array in React using Functional Components/Hooks - reactjs

I am trying to delete an item in an array. However, my delete button is not executing my code and the array remains unchanged. I am not sure what to do.
My code is below:
//App.js
import React, { useState } from "react";
import Overview from "./components/Overview";
function App() {
const [task, setTask] = useState("");
const [tasks, setTasks] = useState([]);
function handleChange(e) {
setTask(e.target.value);
}
function onSubmitTask(e) {
e.preventDefault();
setTasks(tasks.concat(task));
setTask("");
}
//error happening here????---------------------------------------------------
function removeTask(itemId) {
setTasks(prevState => prevState.filter(({ id }) => id !== itemId));
}
return (
<div className="col-6 mx-auto mt-5">
<form onSubmit={onSubmitTask}>
<div className="form-group">
<label htmlFor="taskInput">Enter task</label>
<input
onChange={handleChange}
value={task}
type="text"
id="taskInput"
className="form-control"
/>
</div>
<div className="form-group">
<button type="submit" className="btn btn-primary">
Add Task
</button>
</div>
</form>
<Overview tasks={tasks} removeTask={removeTask} />
</div>
);
}
export default App;
Child Component:
import React from "react";
function Overview(props) {
const { tasks, removeTask } = props;
console.log(tasks)
return (
<>
{tasks.map((task, index) => {
return (
<>
<p key={index}>
#{index + 1} {task}
</p>
//this onClick isn't doing anything-------------------------------------
<button onClick={() => removeTask(index)}>Delete Task</button>
</>
);
})}
</>
);
}
export default Overview;
My 'tasks' state gives me an array, with items inside as strings. However, when I tried to filter the array, that didn't work. So instead of filtering by value, I tried to filter by id/index. Since the index would match it I thought that would remove the item from the array, even if there is just one item, it doesn't remove anything and the delete button just console logs the given array.
Any help would be greatly appreciated.

I think you need to pass the taskId instead of index here
<button onClick={() => removeTask(task.id /*index*/)}>Delete Task</button>
because removeTask function is dealing with taskId not with the index
However it's looks like you don't have id field on tasks even though you assuming it is there in setTasks(prevState => prevState.filter(({ id }) => id !== itemId));, so if you want to keep removing task by index, change removeTask as below.
function removeTask(index) { // remove by index
setTasks(prevState => {
const tasks = [...prevState]; // create new array based on current tasks
tasks.splice(index, 1); // remove task by index
return tasks; // return altered array
});
}
demo

Issue
Your delete method consumes an item id, but you pass it an index in the button's onClick handler.
Solution
Choose one or the other of id or index, and remain consistent.
Using id
function removeTask(itemId) {
setTasks(prevState => prevState.filter(({ id }) => id !== itemId));
}
...
<button onClick={() => removeTask(task.id)}>Delete Task</button>
Using index
function removeTask(itemIndex) {
setTasks(prevState => prevState.filter((_, index) => index !== itemIndex));
}
...
<button onClick={() => removeTask(index)}>Delete Task</button>
Since it doesn't appear your tasks are objects with an id property I suggest adding an id to your tasks. This will help you later when you successfully delete a task from you list since you'll also want to not use the array index as the react key since you expect to mutate your tasks array.
App.js
import { v4 as uuidV4 } from 'uuid';
...
function onSubmitTask(e) {
e.preventDefault();
setTasks(prevTasks => prevTasks.concat({
id: uuidV4(), // <-- generate new id
task
}));
setTask("");
}
function removeTask(itemId) {
setTasks(prevState => prevState.filter(({ id }) => id !== itemId));
}
Child
function Overview({ tasks, removeTask }) {
return (
{tasks.map(({ id, task }, index) => { // <-- destructure id & task
return (
<Fragment key={id}> // <-- react key on outer-most element
<p>
#{index + 1} {task}
</p>
<button onClick={() => removeTask(id)}>Delete Task</button>
</>
);
})}
);
}

Related

React doesn't re-render components after state changes

I'm creating a todo app and I have some problem
I got a feature to add a todo item to favorite and so objects with "favorite: true" should be first at the array of all todos.
useEffect(() => {
// Sort array of objects with favorite true were first
setTodos(todos.sort((x, y) => Number(y.favorite) - Number(x.favorite)));
}, [todos]);
//Add to favorite function
const favoriteHandler = () => {
setTodos(
todos.map((e) => {
if (e.id === id) {
return {
...e,
favorite: e.favorite,
};
}
return e;
})
);
};
<div className="favorite-button" onClick={() => favoriteHandler()}>
{favorite ? (
<img src={FavoriteFilledIcon} alt="Remove from favorite" />
) : (
<img src={FavoriteIcon} alt="Add to favorite" />
)}
</div>
but if click on a favoriteHandler console log tells me that objects with favorite: true is at the start of array, but todos.map doesn't re-render this changes, why?
// App.js
{todos.map((e, i) => (
<TodoItem
completed={e.completed}
id={e.id}
key={i}
text={e.name}
setTodos={setTodos}
todos={todos}
favorite={e.favorite}
/>
))}
There is no re-render because no state is being change.
In favoriteHandler function you are just setting the favorite as the initial favorite you supplied as props. If you're trying to toggle favorite on click you should consider using boolean operator as below.
const favoriteHandler = () => {
setTodos(
todos.map((e) => {
if (e.id === id) {
return {
...e,
favorite: !e.favorite,
};
}
return e;
})
);
};
In terms of sorting you won't need to use useEffect hook but re-write as following:
todos
.sort((x, y) => Number(y.favorite) - Number(x.favorite))
.map((todo) => {
return (
<TodoItem
completed={todo.completed}
id={todo.id}
key={todo.id}
text={todo.text}
setTodos={setTodos}
todos={todos}
favorite={todo.favorite}
/>
);
});
For Improvement in your code I suggest the following.
Try to use a more straightforward variable name like todo and stay away from variables name like e.
Instead of attaching onClick on div tag, use button tags.
When you are trying to map over an array and have a unique key, you should use that key instead of index.

The text won't cross out in Todo app in "React"

I'm beginner and I'm working on Todo app in "React". I made a class with text-decoration: line-through and I want to cross out text when click on check_btn. But I don't know why the text won't cross out.
const Task = ({ setTasks, tasks, id, title }) => {
const deleteHandler = () => {
setTasks(tasks.filter((el) => el.id !== id));
};
const completeHandler = () => {
setTasks(
tasks.filter((item) => {
if (item.id === tasks.id) {
return {
...item,
completed: !item.completed,
};
}
return item;
})
);
};
return (
<div>
<li>
<div className={`task_name ${tasks.completed ? "completed " : ""}`}>
{title}
</div>
<button className="delete_btn" onClick={deleteHandler}>
<FaTimesCircle className="delete_item" />
</button>
<button className="edit_btn">
<FaEdit className="edit_item" />
</button>
<button className="check_btn" onClick={completeHandler}>
<FaCheckCircle className="check_item" />
</button>
</li>
</div>
);
};
tasks.completed will always be undefined. tasks is the array of tasks, not an individual task inside that array.
What you want to do is check the value of task.completed for each task in the tasks array.
You should use map instead of filter in completeHandler
filter is to filter data conditionally
map is to map a current object to another object
In your case, you change completed attribute in existing tasks means mapping data instead of filtering data.
Another problem is you use condition if (item.id === tasks.id) wrongly. It should be if (item.id === id) which is comparing to the current task id.
const Task = ({ setTasks, tasks, id, title }) => {
//find the current task by task id
const currentTask = tasks.find(task => task.id === id)
//if cannot find any task by task id in the current tasks list
//return nothing
if(!currentTask) {
return null
}
const deleteHandler = () => {
setTasks(tasks.filter((el) => el.id !== id));
};
const completeHandler = () => {
setTasks(
//use `map` instead of `filter`
tasks.map((item) => {
//compare to the current task id instead of `tasks.id` (not existing)
if (item.id === id) {
return {
...item,
completed: !item.completed,
};
}
return item;
})
);
};
return (
<div>
<li>
<div className={`task_name ${currentTask.completed ? "completed " : ""}`}>
{title}
</div>
<button className="delete_btn" onClick={deleteHandler}>
<FaTimesCircle className="delete_item" />
</button>
<button className="edit_btn">
<FaEdit className="edit_item" />
</button>
<button className="check_btn" onClick={completeHandler}>
<FaCheckCircle className="check_item" />
</button>
</li>
</div>
);
};

Delete a row from array by component event in that array using React

Below is a basic example of the problem I'm experiencing.
I have a list of items that are in an array. The user can add another item to the list, and it shows on the page. Each item has a delete button and that delete button is a component inside the array item (this is so each item could have a button that does a different action.. in my example the action is deleting, but later it might be "Send" or "Delete" or "Cancel" or "Edit"...)
The trouble is, when I click the "Action" in this case delete, I want to know which item in the array this was. This way, I can get the array index and delete it. Or later grab additional details from the
import React, {useState} from "react"
function App() {
const [list, setList] = useState([])
function addRow(){
let newRow = {
name: "Test",
action: <span onClick={e=>removeThisRow()}>REMOVE</span>
}
setList([
...list,
newRow
])
}
function removeThisRow(){
// need this to remove the specific item from my list array...
console.log("removing...")
}
return (
<div>
{
list.map(item=>(
<div>
{item.name} | {item.action}
</div>
))
}
<div onClick={e=>addRow()}>ADD ROW</div>
</div>
);
}
export default App;
Working and tested
function App() {
const [list, setList] = React.useState([])
function addRow(){
let newRow = {
id: Math.random(),
name: `Test-${Math.random()}`,
action: (id) => <span onClick={()=>removeThisRow(id)}>REMOVE</span>
}
setList([
...list,
newRow
])
}
function removeThisRow(id){
setList(l => l.filter(li => li.id !== id))
}
return (
<div>
{
list.map((item)=>(
<div key={item.id}>
{item.name} | {item.action(item.id)}
</div>
))
}
<div onClick={e=>addRow()}>ADD ROW</div>
</div>
);
}
Id is just random number, I would use something more unique like uuid()
Here's an alternative approach to dynamically adding/editing/removing items from an array. Instead of unnecessarily creating JSX within an array, you can map over the array list and edit/remove the row based upon a unique id.
You can get even more sophisticated by adding two additional input toggles before adding a new row. These two toggles might add an isEditable and isRemovable properties to the row when when it's created. These properties can then be used to dynamically include or exclude buttons when the list is displayed. This is a cleaner approach as you're not recreating the same buttons over and over for each row, but flexible enough to conditionally render them.
On a related note, using the array index as a key is anti-pattern.
Demo Source Code:
Demo: https://q3wph.csb.app/
Code:
import * as React from "react";
import { v4 as uuid } from "uuid";
import Button from "./components/Button";
import Input from "./components/Input";
import Switch from "./components/Switch";
import "./styles.css";
export default function App() {
const [list, setList] = React.useState([]);
const [editRow, setEditRow] = React.useState({
id: "",
value: ""
});
const [newRowValue, setNewRowValue] = React.useState("");
const [newRowIsEditable, setNewRowEditable] = React.useState(true);
const [newRowIsRemovable, setNewRowRemovable] = React.useState(true);
const removeItem = (removeId) => {
setList((prevState) => prevState.filter(({ id }) => id !== removeId));
};
const editItem = (editId) => {
const { name } = list.find((item) => item.id === editId);
setEditRow({ id: editId, value: name });
};
const updateRowItem = ({ target: { value } }) => {
setEditRow((prevState) => ({ ...prevState, value }));
};
const handleRowUpdate = (e) => {
e.preventDefault();
const { id, value } = editRow;
if (!value) {
alert("Please fill out the row input before updating the row!");
return;
}
setList((prevState) =>
prevState.map((item) =>
item.id === id ? { ...item, name: value } : item
)
);
setEditRow({ id: "", value: "" });
};
const addNewRowItem = (e) => {
e.preventDefault();
if (!newRowValue) {
alert("Please fill out the new row item before submitting the form!");
return;
}
setList((prevState) => [
...prevState,
{
id: uuid(),
name: newRowValue,
isEditable: newRowIsEditable,
isRemovable: newRowIsRemovable
}
]);
setNewRowValue("");
setNewRowEditable(true);
setNewRowRemovable(true);
};
return (
<div className="app">
<h1>Dynamically Add/Edit/Remove Row</h1>
{list.length > 0 ? (
list.map(({ id, name, isEditable, isRemovable }) => (
<div className="uk-card uk-card-default uk-card-body" key={id}>
{editRow.id === id ? (
<form onSubmit={handleRowUpdate}>
<Input
placeholder="Add a new row..."
value={editRow.value}
handleChange={updateRowItem}
/>
<Button color="secondary" type="submit">
Update Row
</Button>
</form>
) : (
<>
<h2 className="uk-card-title">{name}</h2>
{isEditable && (
<Button
className="uk-margin-small-bottom"
color="primary"
type="button"
handleClick={() => editItem(id)}
>
Edit
</Button>
)}
{isRemovable && (
<Button
color="danger"
type="button"
handleClick={() => removeItem(id)}
>
Remove
</Button>
)}
</>
)}
</div>
))
) : (
<div>(Empty List)</div>
)}
<form
className="uk-card uk-card-default uk-card-body"
onSubmit={addNewRowItem}
>
<Input
placeholder="Add a new row..."
value={newRowValue}
handleChange={(e) => setNewRowValue(e.target.value)}
/>
<Switch
label="Editable"
handleChange={(e) => setNewRowEditable(Boolean(e.target.checked))}
name="Editable"
value={newRowIsEditable}
/>
<Switch
label="Removable"
handleChange={(e) => setNewRowRemovable(Boolean(e.target.checked))}
name="Removable"
value={newRowIsRemovable}
/>
<Button
className="uk-margin-small-bottom"
color="secondary"
type="submit"
>
Add Row
</Button>
<Button color="danger" type="button" handleClick={() => setList([])}>
Reset List
</Button>
</form>
</div>
);
}
I'm attaching an attribute to the span, named index and accessing that in removeThisRow.
import "./styles.css";
import React, { useState } from "react";
function App() {
const [list, setList] = useState([]);
function addRow() {
let newRow = {
name: "Test",
action: (
<span index={list.length} onClick={(e) => removeThisRow(e)}>
REMOVE
</span>
)
};
setList([...list, newRow]);
}
function removeThisRow(e) {
// need this to remove the specific item from my list array...
const index = e.target.getAttribute("index"); // got the index as a string
// Do whatever you want
}
return (
<div>
{list.map((item) => (
<div>
{item.name} | {item.action}
</div>
))}
<div onClick={(e) => addRow()}>ADD ROW</div>
</div>
);
}
export default App;

Checkbox inside map not getting checked

I'm new to react and trying to create a todo app using react hooks. I have an array todoList which i'm displaying inside unordered list and there is a checkbox for every todo element.Now, issue is the checkbox state is not getting changed on click. What am i missing? in OnChange i tried by directly changing item.isDone property and also i tried using setTodoList as well but in both cases there is nothing happening in UI. useForm is just another module that uses useState.
const App = () => {
const [todoVal, handleChange] = useForm({
todoValue: "",
});
const [todoList, setTodoList] = useState([]);
return (
<div>
<div className="container">
<h1>Todo Item {todoVal.todoValue}</h1>
<div className="row">
<input
type="text"
placeholder="Write a todo"
className="form-control"
required
name="todoValue"
value={todoVal.todoValue}
onChange={handleChange}
/>
<button
className="btn btn-primary btn-lg"
onClick={() => {
setTodoList([
...todoList,
{
id: Math.floor(Math.random() * 20),
isDone: false,
value: todoVal.todoValue,
},
]);
}}
disabled={!todoVal.todoValue.length}
>
Add Todo
</button>
</div>
<div className="row">
<ul className="list-group">
{todoList.map((item, idx) => {
return (
<li key={item.id} className="list-group-item">
<div className="row">
<div className="col-xs-8 px-2">
<input
id={"isDone-" + item.id}
type="checkbox"
name={"isDone-" + item.id}
checked={item.isDone}
onChange={(e) => {
item.isDone = !item.isDone;
}}
/>
<label for={"isDone-" + item.id}>{item.value} - {String(item.isDone)}</label>
</div>
<button
className="btn btn-danger"
onClick={() => {
const list = todoList.filter(
(todoItem) => todoItem.id !== item.id
);
setTodoList(list);
}}
>
Delete
</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
</div>
);
};
export default App;
Just change the onChange in input checkbox to
onChange ={ (e) => {
// item.isDone = !item.isDone; you cannot change item directly as it's immutable
setTodoList(
todoList.map((it) =>
it.id !== item.id ? it : { ...item, isDone: !item.isDone }
)
);
}}
Explaination: item object is immutable, so it is todoList. So when you want to set its property isDone to true you have to create a proxy copy and then use setTodoList.
So we call setTodoList and generate the copy of todoList with map. If id is different from the one that you are checking we keep todoItem (it in my code) as it is, else we create a copy of it with the spread operator {...} and update its isDone property to true.
We could also use immer to generate a proxy mutable object that we can edit directly but I think in this simple case is overkilled.
For the sake:
install immer with npm i immer
on the top: import {produce} from 'immer';
then:
onChange ={ (e) => {
setTodoList(
todoList.map((it) =>
it.id !== item.id ? it : produce(item, (draftItem) => {
draftItem.isDone = !draftItem.isDone;
})
)
);
}}
You already have an index, so you don't need to map through the list. You can create a copy of array and update specific index:
onChande={ () => {
setTodoList((prevTodoList) => {
// create a copy of todoList
const updatedTodoList = [...prevTodoList];
// toggle isDone state at specific index
updatedTodoList[idx].isDone = !updatedTodoList[idx].isDone;
return updatedTodoList;
});
}}
Same thing can be applied to your delete method, so you won't be needing to filter through whole list:
onClick={ () => {
setTodoList((prevTodoList) => {
// create a copy of todoList, needed because splice mutates array
const updatedTodoList = [...prevTodoList];
// remove item at specific index, mutates array
updatedTodoList.splice(idx, 1);
return updatedTodoList;
});
}}

Why Todo List in React erases always the last item?

I've been trying to make a Todo List App Work with React Hooks.
Everything works just fine when I use <span>{todo}</span>. It just delete the element that I click. But when I change <span>{todo}</span> for <input></input>, every 'X' that I click to delete always delete the last element. I just don't know what's happening, as the keys aren't changed.
Todo.js Component:
import React, { useState } from 'react';
const TodoForm = ({ saveTodo }) => {
const[value, setValue] = useState('');
return (
<form
onSubmit={event => {
event.preventDefault();
saveTodo(value);
setValue('');
}}
>
<input onChange={event => setValue(event.target.value)} value={value} />
</form>
)
}
const TodoList =({ todos, deleteTodo }) => (
<ul>
{
todos.map((todo, index) => (
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
))
}
</ul>
);
const Todo = () => {
const [todos, setTodos] = useState([]);
return (
<div className="App">
<h1>Todos</h1>
<TodoForm saveTodo={todoText => {
const trimmedText = todoText.trim();
if(trimmedText.length > 0) {
setTodos([...todos, trimmedText]);
}
}}
/>
<TodoList
todos={todos}
deleteTodo={todoIndex => {
const newTodos = todos.filter((_, index) => index !== todoIndex);
setTodos(newTodos);
}}
/>
</div>
);
};
export default Todo;
It changes the deletion behavior when I change:
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
to:
<li key={index.toString()}>
<input></input>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
Are you sure that this is the case? Or it just seems to behave that way, because you render the input without any values? If I paste your code (and adjust the input to actually include the value of the todo) into a CodeSandbox it deletes the correct element. Please also consider that using indexes as list keys should be seen as the "last resort" (See React docs).
The problem is that you are using index as the key. Therefore the last item (key that stops existing) is removed and all the other items are just updated.
Instead, create some kind of unique id for your todos and use that id as key.

Resources