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.
Related
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>
</>
);
})}
);
}
What's up ?
I'm trying to reproduce the sliding button effect from frontity home page with ReactJS (NextJS).
Sliding buttons from Frontity
I managed to create the sliding button effect BUT I'm struggling with state management.
I have all my objects mapped with a "isActive : true/false" element and I would to create a function that put "isActive : true" on the clicked button BUT put "isActive: false" on all the other buttons.
I don't know the syntax / method for that kind of stuff.
Please, take a look at my codesandbox for more clarity (using react hooks):
https://codesandbox.io/s/busy-shirley-lgx96
Thank you very much people :)
UPDATE: As pointed out above by Drew Reese, even more cleaner/easier is to have just one activeIndex state:
const TabButtons = () => {
const [activeIndex, setActiveIndex] = useState(0);
const handleButtonClick = (index) => {
setActiveIndex(index);
};
return (
<>
<ButtonsWrapper>
{TabButtonsItems.map((item, index) => (
<div key={item.id}>
<TabButtonItem
label={item.label}
ItemOrderlist={item.id}
isActive={index === activeIndex}
onClick={() => handleButtonClick(index)}
/>
</div>
))}
<SlidingButton transformxbutton={activeIndex}></SlidingButton>
</ButtonsWrapper>
</>
);
};
I have made a slight modification of your TabButtons:
const TabButtons = () => {
const [buttonProps, setButtonProps] = useState(TabButtonsItems);
// //////////// STATE OF SLIDING BUTTON (TRANSLATE X ) ////////////
const [slidingbtn, setSlidingButton] = useState(0);
// //////////// HANDLE CLIK BUTTON ////////////
const HandleButtonState = (item, index) => {
setButtonProps((current) =>
current.map((i) => ({
...i,
isActive: item.id === i.id
}))
);
setSlidingButton(index);
};
return (
<>
<ButtonsWrapper>
{buttonProps.map((item, index) => (
<div key={item.id}>
<TabButtonItem
label={item.label}
ItemOrderlist={item.id}
isActive={item.isActive}
onClick={() => HandleButtonState(item, index)}
/>
</div>
))}
<SlidingButton transformxbutton={slidingbtn}></SlidingButton>
</ButtonsWrapper>
</>
);
};
When we click on a button, we set its isActive state to true and all the rest buttons to isActive: false. We also should use state, since we also declared it. Changing state will force component to re-render, also we are not mutating anything, but recreating state for buttons.
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.
I want to make a little react application that saves short text entries. The app shows all published entries and the user can add a new entry by writing and publishing it.
The applcation has an string-array (string[]) type. Every item in the array is an entry that has to be displayed in the frontend entries list.
I know that I can't push to the array because that doesn't changes the state directly (and react doesn't notice that it have to be re-rendered). So i am using this way to get the new state: oldState.concat(newEntry). But React doesn't re-render it.
Here is my whole react code:
function App() {
const [entries, setEntries] = useState([] as string[])
const publish = (entry: string) => {
setEntries(entries.concat(entry))
}
return (
<div>
<Entries entries={entries} />
<EntryInput publish={publish} />
</div>
)
}
function Entries(props: { entries: string[] }) {
return (
<div className="entries">
{props.entries.map((v, i) => { <EntryDisplay msg={v} key={i} /> })}
</div>
)
}
function EntryInput(props: { publish: (msg: string) => void }) {
return (
<div className="entry-input">
<textarea placeholder="Write new entry..." id="input-new-entry" />
<button onClick={(e) => { props.publish((document.getElementById("input-new-entry") as HTMLTextAreaElement).value) }}>Publish</button>
</div>
)
}
function EntryDisplay(props: { msg: string }) {
return (
<div className="entry">{props.msg}</div>
)
}
const reactRoot = document.getElementById("react-root")
ReactDOM.render(<App />, reactRoot)
State is updated correctly, the return keyword is missing here:
function Entries(props: { entries: string[] }) {
return (
<div className="entries">
{props.entries.map((v, i) => {
// add missing 'return'
return <EntryDisplay msg={v} key={v} />
})}
</div>
)
}
Also, don't use array indexes as keys.