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;
});
}}
Related
The useState hook is not working for me. In props.data I am getting the array.
I am trying to update the array of objects, but rendering is not working:
const MenuList = (props)=>{
const [enteredData,setEnteredData] = useState(props.data);
const amountChangeHandler = (event)=>{
const findElementId = event.target.attributes.alter.value;
const item = props.data.find(item => item.id === findElementId);
item.amount = parseInt(document.getElementById(findElementId).value);
console.log(props.data);
setEnteredData(props.data);
}
return(
<ul>
{enteredData.map((item)=><li key={item.id} className={classes['cart-item']}>
<div>
<h2>{item.mealName}</h2>
<div className={classes.summary}><i>{item.mealDescription}</i>
<span className={classes.price}>${item.price}</span>
<span className={classes.amount}>x {item.amount}</span>
</div>
</div>
<div className={classes.actions}>
<div><label htmlFor={item.id}>Amount </label>
<input type='number' id={item.id} defaultValue='1' ></input>
</div>
<button alter={item.id} onClick={amountChangeHandler}>+Add</button>
</div>
</li>) }</ul>
)
}
export default MenuList;
This is because react is not identifying that your array has changed. Basically react will assign a reference to the array when you define it. But although you are changing the values inside the array, this reference won't be changed. Because of that component won't be re rendered.
So you have to notify react, that the array has updated.
This can be achieve by updating the enteredData by iterating the props.data
const [enteredData,setEnteredData] = useState(props.data);
const amountChangeHandler = (event)=>{
const findElementId = event.target.attributes.alter.value;
const item = props.data.find(item => item.id === findElementId);
item.amount = parseInt(document.getElementById(findElementId).value);
console.log(props.data);
setEnteredData([...props.data]);
}
return(
<ul>
{enteredData.map((item)=><li key={item.id} className={classes['cart-item']}>
<div>
<h2>{item.mealName}</h2>
<div className={classes.summary}><i>{item.mealDescription}</i>
<span className={classes.price}>${item.price}</span>
<span className={classes.amount}>x {item.amount}</span>
</div>
</div>
<div className={classes.actions}>
<div><label htmlFor={item.id}>Amount </label>
<input type='number' id={item.id} defaultValue='1' ></input>
</div>
<button alter={item.id} onClick={amountChangeHandler}>+Add</button>
</div>
</li>) }</ul>
)
Issue
The issue here is state/reference mutation.
// Currently `enteredData` is a reference to `props.data
const [enteredData, setEnteredData] = useState(props.data);
const amountChangeHandler = (event) => {
const findElementId = event.target.attributes.alter.value;
// `item`, if found, is a reference to an element in the
// `enteredData`, a.k.a. `props.data`, array
const item = props.data.find(item => item.id === findElementId);
// This is the mutation(!!), directly mutating the item reference
item.amount = parseInt(document.getElementById(findElementId).value);
// props.data is still the same reference as `enteredData`
setEnteredData(props.data);
};
Direct mutation of existing state.
A new array reference is never created for React's reconciliation process to use shallow reference equality to trigger a rerender.
Mutating the props.data array in the child also mutates the source in the parent component, which should be avoided at all costs.
Solution
Use a functional state update to correctly access, and update from, the previous state value. You should shallow copy all state, and nested state, that is being updated.
Example:
const [enteredData, setEnteredData] = useState(props.data);
const amountChangeHandler = (event) => {
const findElementId = event.target.attributes.alter.value;
setEnteredData(data => data.map(
item => item.id === findElementId
? {
...item,
amount: parseInt(document.getElementById(findElementId).value, 10)
}
: item)
);
};
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 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>
</>
);
})}
);
}
Trying to do a dropzone upload component that allows user to tag each image uploaded using an input field..
Problem is the tags used for the first image is also loaded in the tag for the second image...
Ideally, each image should have its own "set" of tags on upload. Not sure what am I missing in terms of reusing the TagInput component.
Screenshot below to show the erroneous behavior:
Dropzone.js
const [tags, setTags] = useState({});
const addTagHandler = (aTag, index) => {
let result;
if (Object.keys(tags).length === 0) {
// if nothing, create.
result = { [index]: [aTag] };
} else {
//check index, if index exists, push to index. else, create index
if (index < Object.keys(tags).length) {
result = { ...tags, [index]: [...tags[index], aTag] };
} else {
result = { ...tags, [index]: [aTag] };
}
}
setTags(result);
};
<div className="file-display-container">
{validFiles.map((aFile, index) => (
<div className="file-status-bar" key={index}>
<div>
{previewUrl && (
<div className="file-preview">
<img src={previewUrl[index]} alt="image" />
</div>
)}
<span
className={`file-name ${aFile.invalid ? "file-error" : ""}`}
>
{aFile.name}
</span>
<span className="file-size">({fileSize(aFile.size)})</span>{" "}
{aFile.invalid && (
<span className="file-error-message">({errorMessage})</span>
)}
</div>
<TagInput
tags={tags[index]}
onAdd={(aTag) => addTagHandler(aTag, index)}
onDelete={deleteTagHandler}
/>
<div
className="file-remove"
onClick={() => removeFile(aFile.name)}
>
X
</div>
</div>
))}
</div>
TagInput.js
const [input, setInput] = useState("");
const _keyPressHandler = (event) => {
if (event.key === "Enter" && input.trim() !== "") {
onAdd(input.trim());
setInput("");
}
};
return (
<div className="taginput">
{tags &&
tags.map((aTag, index) => (
<Tag
key={aTag + index}
label={aTag}
onClickDelete={() => onDelete(index)}
/>
))}
<Input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={_keyPressHandler}
placeholder="Insert tag here"
/>
</div>
);
};
You are passing the same array of tags to each TagInput component. They need to be separated in some fashion.
What you need to do is create a tag array for each input. You could do this by using an object based on the file key.
Something like this should work. Just pass the key of the file to each handler so it updates the appropriate array.
const [tags, setTags] = useState({});
const addTagHandler = (key, tag) => {
setTags({...tags, [key]: [...tags[key], tag]});
};
const deleteTagHandler = (key, i) => {
setTags({...tags, [key]: tags[key].filter((_, index) => index !== i));
};
Update your tag component to use the key i like so.
<TagInput
tags={tags[i]}
onAdd={tag => addTagHandler(i,tag)}
onDelete={tagIndex => deleteTagHandler(i,tagIndex)}
/>
Your issue is in how you are setting the tags state. In your line const [tags, setTags] = useState([]);, you are setting tags as a single array of strings, which is being used by every consequent image with a tag, as seen in the line
<TagInput
tags={tags}
onAdd={addTagHandler}
onDelete={deleteTagHandler}
/>
where you are reusing the same tags state among all the divs in .file-display-container.
A solution for this would be to instead have an array of strings inside the state array. (tags: [["tag1", "tag2"], ["image2Tag1"], ....])
so in your old line where you set tags={tags} for each TagInoput, you would instead set is as tags={tags[i]}(the index is i in your map function).
you would need to adjust the tag handler functions accordingly. (#Todd Skelton's answer provides a fantastic way to handle it)
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.