How to make checkbox change specific object property to false - javascript

Started this todo app in react that takes input and adds the input to the array of objects. Each todo item has a checkbox next to it. I want when the checkbox is checked, completed of the specific property to change to either true or false depending on the initial value but I keep running to errors.
See function isCompleted and help me find a way to do this.
const Todos = () => {
const [todo, setTodo] = useState([
{
id: 1,
title: "Go to store",
completed: true
},
{
id: 2,
title: "Buy groceries",
completed: false
},
{
id: 3,
title: "Go to dinner with wife",
completed: true
}
]);
const [work, setWork] = useState("");
const newTodo = e => {
setWork(e.target.value);
};
const addTodo = e => {
e.preventDefault();
setTodo(prevTodo => [
...prevTodo,
{ id: prevTodo.length + 1, title: work, completed: false }
]);
setWork("");
};
const isCompleted = () => {
setTodo(todo.map(todos => {
if (todos.completed) {
todos.completed = false
}
else {
todos.completed = true
}
}))
};
return (
<div>
<form onSubmit={addTodo}>
<input
type="text"
value={work}
onChange={newTodo}
className="inputText"
/>
<button>Add</button>
</form>
<div>
{todo.map(todos => (
<TodoItem
key={todos.id}
title={todos.title}
completed={todos.completed}
id={todos.id}
isCompleted={isCompleted}
/>
))}
</div>
</div>
);
};

You want to pass in the id of the specific todo to mark just that one as completed.
const isCompleted = (id) => {
setTodo(todo.map(todos => {
if (todos.id === id) {
todos.completed = true;
}
return todos;
}))
};
...
<TodoItem
key={todos.id}
title={todos.title}
completed={todos.completed}
id={todos.id}
isCompleted={() => isCompleted(todos.id)}
/>

Related

Why is useMemo not rerendering even though the dependancy being passed into it is changing?

So I have a component TaskList:
const [incompleteTasks, setIncompleteTasks] = useState<any[]>([]);
useMemo(() => {
const filteredTasks = TaskStore.tasks.filter(
(task: any) => !task.completed
);
setIncompleteTasks(filteredTasks);
}, [TaskStore.tasks]);
and the observable state is passed as a dependancy from TaskStore:
public tasks: Task[] = [];
constructor() {
makeAutoObservable(this);
}
#action setCompleted = (task: Task, completed: number) => {
const index = this.tasks.indexOf(task);
if (index !== -1) {
this.tasks[index].completed = !!completed;
}
};
I thought the way useMemo() works is that it caches the calculation in the first parameter(so the filtered array), and then the second parameter is the dependancy which triggers a useMemo() to return another calculation of the first parameter if it changes. Is my understanding wrong? Or am I not doing it correctly?
You've said you believe TaskStore.tasks to be observable because you're using makeAutoObservable, so in theory that shouldn't be the problem. My Mobx is very rusty (I haven't used it in years, though I like the idea of it), but since you're using Mobx, I suspect you want a computed, not useMemo, for what you're doing. See the end of the answer for an example using Mobx where incompleteTasks is a computed.
Using useMemo
But if you want to use useMemo:
Using useMemo's callback to call a state setter is incorrect. Instead, incompleteTasks should be the result of useMemo, not a state member, because it's derived state, not primary state:
const incompleteTasks = useMemo(
() => TaskStore.tasks.filter((task: any) => !task.completed),
[TaskStore.tasks]
);
useMemo will only call your callback if TaskStore.tasks changes;¹ otherwise, when called, it will return the previously-returned value instead. So your code only filters when necessary.
Here's a really simple non-Mobx example of derived state:
const { useState, useCallback, useMemo, useEffect } = React;
const initialTasks = [
{ id: 1, text: "first task", completed: false },
{ id: 2, text: "second task", completed: false },
{ id: 3, text: "third task", completed: false },
{ id: 4, text: "fourth task", completed: false },
{ id: 5, text: "fifth task", completed: false },
{ id: 6, text: "sixth task", completed: false },
];
const Task = ({ task, onChange }) => {
const handleChange = ({ currentTarget: { checked } }) => onChange(task, checked);
return (
<label>
<input type="checkbox" checked={task.completed} onChange={handleChange} /> {task.text}
</label>
);
};
const Example = () => {
const [counter, setCounter] = useState(0);
const [tasks, setTasks] = useState(initialTasks);
const incompleteTasks = useMemo(() => {
console.log(`Updating incompleteTasks.`);
return tasks.filter((task) => !task.completed);
}, [tasks]);
const updateTask = useCallback((task, completed) => {
setTasks((tasks) => tasks.map((t) => (t === task ? { ...t, completed } : t)));
}, []);
// A pointless counter just to show that `incompleteTasks` isn't recreated
// on every render
useEffect(() => {
const timer = setInterval(() => {
setCounter((c) => c + 1);
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
// ...code to add/remove tasks not shown for brevity...
return (
<div>
<div>
Counter: {counter}
</div>
<h3>All tasks:</h3>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<Task task={task} onChange={updateTask} />
</li>
))}
</ul>
<h3>Incomplete tasks:</h3>
<ul>
{incompleteTasks.map((task) => (
<li key={task.id}>
<Task task={task} onChange={updateTask} />
</li>
))}
</ul>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<div style="height: 50rem"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Notice how even though the component is re-rendered every time counter changes, incompleteTasks is only re-calculated when tasks changes (for instance, because one of the tasks is marked completed/uncompleted).
Using Mobx's computed
But I suspect you don't want to use useMemo at all. Instead, you probably want to make incompleteTasks a Mobx computed. I haven't used Mobx in several years, but here's an example I put together using their documentation, particular this, this, and this:
const { useState, useCallback, useEffect } = React;
const { makeAutoObservable, action } = mobx;
const { observer, computed } = mobxReactLite;
class TaskStore {
tasks = [];
constructor() {
makeAutoObservable(this);
}
get incompleteTasks() {
console.log("Computing incompleteTasks...");
return this.tasks.filter((task) => !task.completed);
}
}
const taskStore = new TaskStore();
taskStore.tasks.push(
{ id: 1, text: "first task", completed: false },
{ id: 2, text: "second task", completed: false },
{ id: 3, text: "third task", completed: false },
{ id: 4, text: "fourth task", completed: false },
{ id: 5, text: "fifth task", completed: false },
{ id: 6, text: "sixth task", completed: false }
);
const Task = ({ task, onChange }) => {
const handleChange = ({ currentTarget: { checked } }) => onChange(task, checked);
return (
<label>
<input type="checkbox" checked={task.completed} onChange={handleChange} /> {task.text}
</label>
);
};
const Example = observer(({ taskStore }) => {
const [counter, setCounter] = useState(0);
const updateTask = useCallback(action((task, completed) => {
task.completed = !task.completed;
}), []);
// A pointless counter just to show that `incompleteTasks` isn't recreated
// on every render
useEffect(() => {
const timer = setInterval(() => {
setCounter((c) => c + 1);
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
// ...code to add/remove tasks not shown for brevity...
return (
<div>
<div>Counter: {counter}</div>
<h3>All tasks:</h3>
<ul>
{taskStore.tasks.map((task) => (
<li key={task.id}>
<Task task={task} onChange={updateTask} />
</li>
))}
</ul>
<h3>Incomplete tasks:</h3>
<ul>
{taskStore.incompleteTasks.map((task) => (
<li key={task.id}>
<Task task={task} onChange={updateTask} />
</li>
))}
</ul>
</div>
);
});
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example taskStore={taskStore} />);
<div id="root"></div>
<div style="height: 50rem"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mobx/6.7.0/mobx.umd.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mobx-react-lite/3.4.0/mobxreactlite.umd.development.js"></script>
¹ In theory. The useMemo documentation says it's only a performance optimiation, not a semantic guarantee, but that's all you need for this code.
Your understanding of useMemo is correct. It does cache the result of a calculation between re-renders.
Your code should be more like
return useMemo(() => {
const filteredTasks = TaskStore.tasks.filter(
(task: any) => !task.completed
);
setIncompleteTasks(filteredTasks);
}, [TaskStore.tasks]);

Show and Hide Condition in React

I have a simple problem here which I can't figure out. I wanted to hide menus depending on the condition.
For example if status contains at least one "Unlinked". "All unlinked images" menu should appear. I did used .some and I wonder why it doesn't return a boolean.
Codesandbox is here Click here
const showDeleteAllInvalidButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Invalid");
};
const showDeleteAllUnlinkedButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Unlinked");
};
The methods do return a boolean. But in the menus array you are assigning a function reference not the result -
show: showDeleteAllInvalidButton // function reference
show is now assigned a reference to the function showDeleteAllInvalidButton not the result of productImages?.some. You need to invoke the functions when assigning -
show: showDeleteAllInvalidButton() // result of productImages?.some
In your menus object you have a key that contains a function, so if you want this function to filter out your elements you need to execute the show method in side the filter method.
import React, { useState } from "react";
import Button from "#mui/material/Button";
import MenuItem from "#mui/material/MenuItem";
import KeyboardArrowDownIcon from "#mui/icons-material/KeyboardArrowDown";
import CustomMenu from "../../Menu";
const products = [
{
productName: "Apple",
productImages: [
{
status: "Unlinked"
}
]
},
{
productName: "Banana",
productImages: [
{
status: "Unlinked"
}
]
},
{
productName: "Mango",
productImages: [
{
status: "Unlinked"
},
{
status: "Unlinked"
}
]
}
];
const HeaderButtons = () => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const showDeleteAllInvalidButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Invalid");
};
const showDeleteAllUnlinkedButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Unlinked");
};
const menus = [
{
id: 1,
name: "Invalid images",
action: () => {
handleClose();
},
show: showDeleteAllInvalidButton
},
{
id: 2,
name: "Unlinked images",
action: () => {
handleClose();
},
show: showDeleteAllUnlinkedButton
},
{
id: 3,
name: "All images",
action: () => {
handleClose();
},
show: () => true // not that I changed it to a function for consistency, but you can check for type in the filter method instead of running afunction
}
];
return (
<div>
<Button
color="error"
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
variant="outlined"
onClick={handleClick}
endIcon={<KeyboardArrowDownIcon />}
>
Options
</Button>
<CustomMenu anchorEl={anchorEl} open={open} onClose={handleClose}>
{menus
.filter((e) => e.show()) // here is your mistake
.map(
({
id = "",
action = () => {},
icon = null,
name = "",
divider = null
}) => (
<>
<MenuItem key={id} onClick={action} disableRipple>
{icon}
{name}
</MenuItem>
{divider}
</>
)
)}
</CustomMenu>
</div>
);
};
export default HeaderButtons;
In your code, it will always render because your filter functions are evaluating as truth.

Set acitve classname to multiple items in react js (map) and remove

I need to set the active classname to multiple onclick items inside a .map
I need the list of active items that were clicked
The items that were clicked will be highlighted in yellow, and when i click the same item again it should be removed from active list items.
const [data, setData] = useState([]);
const [activeIndicies, setActiveIndicies] = useState(() =>
data?.map(() => false)
);
useEffect(() => {
// This data is coming from the API response
const data = [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
{ id: 3, name: "three" }
];
setData(data);
}, []);
return statement
onClick={() => {
setActiveIndicies(
activeIndicies.map((bool, j) => (j === index ? true : bool))
);
}}
Code Sandbox
Thank you.
try this one:
import "./styles.css";
import React, { useState, useEffect } from "react";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "one", active: false },
{ id: 2, name: "two", active: false },
{ id: 3, name: "three", active: false }
]);
return (
<div className="App">
<h2>Set active className to multiple items on .map</h2>
{data?.map((item, index) => {
return (
<p className={data[index].active ? "selected" : "notselected"}
onClick={() => {
setData((prevState) =>
_.orderBy(
[
...prevState.filter((row) => row.id !== item.id),
{ ...item, active: !item.active }
],
["name"],
["asc"]
)
);
}}
>
{item.name}
</p>
);
})}
</div>
);
}
You can acheive this by simply making some minor changes to your code:
// Changing the state value to an object so that it can
// store the active value for exact item ids
const [activeIndicies, setActiveIndicies] = useState({});
Then inside of .map()
....
// Checking if there is any value for the item id which is being mapped right now.
const selected = activeIndicies[item.id];
return (
<p
className={selected ? "selected" : "notselected"}
onClick={() => {
/* Then setting the state like below where it toggles
the value for particular item id. This way if item is
selected it will be deselected and vice-versa.
*/
setActiveIndicies((prevState) => {
const newStateValue = !prevState[item.id];
return { ...prevState, [item.id]: newStateValue };
});
}}
// Key is important :)
key={item.id}
>
{item.name}
</p>
);
Hello, friends!
I solved this problem in a more convenient way for me )
const data = [
{ id: 1, name: "Ann", selected: true },
{ id: 2, name: "Serg", selected: false },
{ id: 3, name: "Boris", selected: true },
];
//you don't even need to have a boolean field in the object -
//it will be added by itself on the first click on the element
// const data = [{ name:"Ann", id:1}, { name:"Serg", id:2 },{ name:"Boris", id:3 },]
const [users, setUsers] = useState(data); // no square brackets-[data]
function handleActive(item) {
setUsers((prev) => {
return prev.map((itemName) => {
if (itemName.name === item.name) {
return { ...itemName, selected: !itemName.selected };
// itemName.selected = !itemName.selected // or so
}
return itemName;
});
});
}
return (
{
users.map((item, i) => {
// let current = item.selected === true
// or so -> className={current === true ? 'users': ''}
return (
<div
onClick={() => handleActive(item)}
className={item.selected === true ? "active" : ""}
key={i}
>
{item.name}
</div>
);
})
}
);

React / Typescript : pushing obj into array of object and undefined type

I'm beginnig my journey into TypeScript in React and to experiment what I've learn, I've try a simple Todo App.
Everything is working fine except ONE things !
When I'm pushing 'newTask'
When I'm hovering 'newTask' here's the hint (Google Trad from French) :
The 'Todo | undefined 'is not attributable to the parameter of type' Todo '.
Cannot assign type 'undefined' to type 'Todo'.
I guess it's related to something here :
let [newTask, setNewTask] = useState<Todo>();
because if I type useState<any>(); I don't have any error..
Here's the full code :
import React, { useState } from "react";
// INTERFACES
interface Todo {
id: number;
text: string;
completed: boolean;
}
export const TodoComponent = () => {
// STATE
const initialTodos: Todo[] = [
{ id: 0, text: "Todo 1", completed: false },
{ id: 1, text: "Todo 2", completed: true },
{ id: 2, text: "Todo 3", completed: false },
];
const [todos, setTodos] = useState<Todo[]>(initialTodos);
let [newTask, setNewTask] = useState<Todo>();
// ACTIONS
const handleClickOnComplete = (id: number, completed: boolean) => {
const newTodos = [...todos];
newTodos[id].completed = !completed;
setTodos(newTodos);
};
const handleRemove = (todo: Todo) => {
const newTodos = todos.filter((t) => t !== todo);
setTodos(newTodos);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setNewTask({
id: todos.length,
text: event.target.value,
completed: false,
});
};
const handleSubmitNewTodo = () => {
const newTodos = [...todos];
console.log(newTask, newTodos);
newTodos.push(newTask);
setTodos(newTodos);
};
return (
<div>
<h1>Todo App !</h1>
<div>
{todos.map((todo) => {
return (
<div key={todo.id}>
{todo.id} - {todo.text} -{" "}
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleClickOnComplete(todo.id, todo.completed)}
/>
<button onClick={() => handleRemove(todo)}>Remove task</button>
</div>
);
})}
</div>
<hr />
<div>
<input placeholder="Add todo" type="text" onChange={handleChange} />
<button onClick={handleSubmitNewTodo}>Add todo</button>
</div>
</div>
);
};
Problem is in handleSubmitTodo
Thanks for your help and advices.
Take care.
Updated
You can try this:
const handleSubmitNewTodo = () => {
let newTodos = [...todos];
console.log(newTask, newTodos);
if (newTask) {
newTodos.push(newTask);
setTodos(newTodos);
setNewTask(undefined);
}
};
I have added the setNewTask to undefined to maintain the initial state after adding the new todo to the todo list.
Adding a condition around the push seems to do the trick.
const handleSubmitNewTodo = () => {
const newTodos = [...todos];
if (newTask) {
newTodos.push(newTask);
}
setTodos(newTodos);
};

How to filter an array and add values to a state

I have the current state as:
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
I map through the state and display the data in a div and call a onClicked function to toggle the isChecked value on click:
const clickData = index => {
const newDatas = [...data];
newDatas[index].isChecked = !newDatas[index].isChecked;
setData(newDatas);
const newSelected = [...selected];
const temp = datas.filter(isChecked==true) // incomplete code, struggling here.
const temp = datas.isChecked ?
};
I have another empty state called clicked:
const[clicked, setClicked] = setState([]). I want to add all the objected whose isChecked is true from the datas array to this array. How can I do this?
I just add checkBox & onChange event instead of using div & onClick event for your understanding
import React, { useState, useEffect } from "react";
import "./style.css";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
const [clicked, setClicked] = useState([]);
const clickData = index => {
let tempData = data.map(res => {
if (res.id !== index) {
return res;
}
res.isChecked = !res.isChecked;
return res;
});
setClicked(tempData.filter(res => res.isChecked));
};
useEffect(() => {
setClicked(data.filter(res => res.isChecked));
}, []);
return (
<div>
{data.map((res, i) => (
<div key={i}>
<input
type="checkbox"
checked={res.isChecked}
key={i}
onChange={() => {
clickData(res.id);
}}
/>
<label>{res.name}</label>
</div>
))}
{clicked.map(({ name }, i) => (
<p key={i}>{name}</p>
))}
</div>
);
}
https://stackblitz.com/edit/react-y4fdzm?file=src/App.js
Supposing you're iterating through your data in a similar fashion:
{data.map((obj, index) => <div key={index} onClick={handleClick}>{obj.name}</div>}
You can add a data attribute where you assign the checked value for that element, so something like this:
{data.map((obj, index) => <div key={index} data-checked={obj.isChecked} data-index={index} onClick={handleClick}>{obj.name}</div>}
From this, you can now update your isClicked state when the handleClick function gets called, as such:
const handleClick = (event) => {
event.preventDefault()
const checked = event.target.getAttribute("data-checked")
const index = event.target.getAttribute("data-index")
// everytime one of the elements get clicked, it gets added to isClicked array state if true
If (checked) {
let tempArr = [ ...isClicked ]
tempArr[index] = checked
setClicked(tempArr)
}
}
That will let you add the items to your array one by one whenever they get clicked, but if you want all your truthy values to be added in a single click, then you simply need to write your handleClick as followed:
const handleClick = (event) => {
event.preventDefault()
// filter data objects selecting only the ones with isChecked property on true
setClicked(data.filter(obj => obj.isChecked))
}
My apologies in case the indentation is a bit off as I've been typing from the phone. Hope this helps!

Categories