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.
Related
In a small React app, I'm trying to add delete functionality via a button for a list. Presently, I'm attempting this through the deleteItem function, which makes use of array.splice prototype method.
However, I'm encountering the error, Too many re-renders. React limits the number of renders to prevent an infinite loop.. What is the cause of this error? Shouldn't this function only be invoked once, when the button is clicked?
And how can I resolve this error?
import "./styles.css";
import React, { useState, Fragment } from "react";
export default function App() {
const [items, setItems] = useState(["first item"]);
const [newItem, setNewItem] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
setItems([newItem, ...items]);
};
const handleChange = (event) => {
setNewItem(event.target.value);
};
const deleteItem = (i) => {
setItems(items.splice(i,1))
}
return (
<div>
<form>
<input type="text" value={newItem} onChange={handleChange} />
<input type="button" value="submit" onClick={handleSubmit} />
</form>
<ul>
{items.map((i) => {
return (
<Fragment>
<li>{i}</li>
<button
onClick= {() => deleteItem(i)}> // Amr recommendation
delete
</button>
</Fragment>
);
})}
</ul>
</div>
);
}
Edit: I've taken user, Amr's, recommendation and added a anonymous arrow function to the button. However, a new issue has arisen. I can delete any item up until there exists only one item in the array. The final item cannot be deleted. Why is this?
you are passing function reference on the onClick handler, change it to an arrow function that triggers the delete method onClick= {()=>deleteItem(i)}>
second thing is that you should add keys to your the parent component when you Map over components to prevent unnecessary behavior.
and the last thing is that in your delete method, you are using Array.prototype.splice(), which returns the item that will be removed, from the items, your requested/ required behavior can be achieved through the Array.prototype.filter() method
const deleteItem = (i) => {
setItems(items.filter((item) => item !== i));
};
This is the final result, it should work fine.
import React, { useState, Fragment } from "react";
export default function App() {
const [items, setItems] = useState(["first item"]);
const [newItem, setNewItem] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
setItems([...items, newItem]);
};
const handleChange = (event) => {
setNewItem(event.target.value);
};
const deleteItem = (i) => {
setItems(items.filter((item) => item !== i));
};
console.log(items);
return (
<div>
<form>
<input type="text" value={newItem} onChange={handleChange} />
<input type="button" value="submit" onClick={handleSubmit} />
</form>
<ul>
{items.map((i, idx) => {
return (
<div key={idx}>
<li>{i}</li>
<button onClick={() => deleteItem(i)}>delete</button>
</div>
);
})}
</ul>
</div>
);
}
you can use following code for deleting from an array. it copies 'items' array and delete one item and after that setstate new array.
it prevent re-render whole component,do operations on copy of state and setstate final result.
const deleteItem = (i) => {
let newItems=[...items]
newItems.splice(i,1)
setItems(newItems)
};
import React from 'react'
import { useState, useEffect } from 'react'
import axios from 'axios'
const Home = () => {
const getSongs = () => {
axios.get('http://localhost:8000/api/songs/')
.then(res => setSongs(res.data))
}
let [songs, setSongs] = useState([])
let [paused, setPause] = useState(true)
useEffect(() => {
getSongs()
}, [])
const toggleSong = (id) => {
const x = document.getElementById(id)
if (x.paused){
x.play()
setPause(false)
} else {
x.pause()
setPause(true)
}
}
// Got rid of the functions that are not needed
return (
<>
{
songs.map(song =>
(
<div className='music-controller' key={song.id}>
<div id={'songDiv'} style={{cursor: 'pointer'}} onClick={(e) => changeSongTime(e, song.id)}>
<div id={`songTime-${song.id}`}></div>
</div>
<div className="music-controller-body">
<div className="music-controller-header">
<h2>{song.title}</h2>
<p><small>{song.genre}</small></p>
</div>
<div className="controls">
// here <----------------------
<i unique={song.id} className={`fas fa-${paused ? 'play' : 'pause'}`} onClick={() => toggleSong(song.id)}></i>
<audio id={song.id} onTimeUpdate={() => songTime(song.id)}>
<source src={`http://localhost:8000/api/songs/audio/${song.id}`} />
</audio>
</div>
</div>
</div>
))}
</>
)
}
export default Home
Whenever I click on a specific i element all of the i elements that were not clicked on get changed too.. to put it simply when I click on the 1st i element only its className should change, but all of the i elements classNames are affected, what is causing this?
I think you should use event.target
const handlePlay = (song) => {
song.play();
};
const handlePause = (song) => {
song.pause();
};
...
<div className="controls">
<i
onMouseOver={(e) => handlePlay(e.target)}
onMouseLeave={(e) => handlePause(e.target)}
className={`fas fa-${paused ? 'play' : 'pause'}`}
onClick={() => toggleSong(song.id)}>
</i>
<audio id={song.id} onTimeUpdate={() => songTime(song.id)}>
<source src={`http://localhost:8000/api/songs/audio/${song.id}`} />
</audio>
</div>
I don't think Toggle would work in this case, an action should happen so it knows when it should stop.
Can you put console in toggleSong function at top and check if you are getting correct id. If you are not getting single Id then work is needed with onClick. So, after that also try passing id like this
onClick={(song?.id) => toggleSong(song?.id)}
then see console again and look for correct id if it is displayed or not. I think your className is not updating due to this issue.
One thing more you can try at end is replacing with this
const x = id; //without document.getElementById
const toggleSong = (e, id) => {
const x = document.getElementById(id)
const button = e.currentTarget
if (x.paused){
x.play()
button.className = 'fas fa-pause'
} else {
x.pause()
button.className = 'fas fa-play'
}
}
<i unique={song.id} className='fas fa-play' onClick={(e) => toggleSong(e, song.id)}></i>
I fixed this by just getting the current target with event.currentTarget and change its className accordingly!
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;
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;
});
}}
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>
</>
);
})}
);
}