I have an array of objects in todos state hook. And I want to change a single property if it is completed. Seems I can update it but without using setTodos. I am a React beginner.
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const addTodoHandler = (e) => {
e.preventDefault();
if (input.length < 2) return;
setTodos([...todos, { id: Date.now(), text: input, isComplete: false }]);
setInput("");
};
const removeHandler = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const completeHandler = (id) => {
// working without setTodo()
// errors when added setTodo()
todos.map((todo) =>
todo.id === id ? console.log((todo.isComplete = !todo.isComplete)) : ""
);
};
<div className="todolist">
{todos.map((todo) => (
<Todo
key={todo.id}
id={todo.id}
text={todo.text}
removeHandler={removeHandler}
completeHandler={completeHandler}
isComplete={todo.isComplete}
/>
))}
</div>
To fix this inside completeHandler() first create a new array using map() method and inside map() method update the isComplete for the current todo and simply return the updated value like:
var updatedTodos = todos.map((todo) => todo.id === id ? {
...todo,
isComplete: !todo.isComplete
} : todo);
Then inside setTodos() just return this new updatedTodos array like:
setTodos(updatedTodos);
You can also do this in one-line like:
setTodos(todos.map((todo) => todo.id === id ? { ...todo, isComplete: !todo.isComplete } : todo));
But the previous code provides more readability and also helps in better debugging if you want to check each variable line by line.
Related
what my issue is
Scenario 1: It is not opening sub-lists after searching or removing any list name from the search bar
Scenario 2: After searching any list name in the search bar that is already selected, then after searching that selected list it is showing that list but its checkbox is not selected.
so what do I need after searching list name in the search bar if that list has a sub-list for example if I New Watchlists then I want to show that sub-list also that present under this list after searching in the search bar but right it coming with empty list you can see image below..
const hasSearchTerm = (n, searchTerm) =>
n.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1;
const filterData = (arr, searchTerm) =>
arr?.filter(
(n) =>
hasSearchTerm(n.title, searchTerm) ||
filterData(n.children, searchTerm)?.length > 0
);
function filteredTreeData(data, searchString, checkedKeys, setExpandedTree) {
let keysToExpand = [];
const filteredData = searchString
? filterData(data, searchString).map((n) => {
keysToExpand.push(n.key);
return {
...n,
children: filterData(n.children, searchString, checkedKeys)
};
})
: data;
setExpandedTree([...keysToExpand]);
return filteredData;
}
const Demo = () => {
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [searchValue, setSearchValue] = useState("");
const [tree, setTree] = useState(treeData);
const onExpand = (expandedKeysValue) => {
console.log("onExpand", expandedKeysValue); // if not set autoExpandParent to false, if children expanded, parent can not collapse.
// or, you can remove all expanded children keys.
setExpandedKeys(expandedKeysValue);
setAutoExpandParent(false);
};
const onCheck = React.useCallback(
(checkedKeysValue, e) => {
if (e.checked) {
if (e.node?.children?.length) {
setCheckedKeys(
_.union(
checkedKeys,
_.cloneDeep([
...e.node.key,
...e.node.children.map((child) => child.key)
])
)
);
} else {
setCheckedKeys(_.union(checkedKeys, [e.node.key]));
}
} else {
if (e.node?.children?.length) {
setCheckedKeys(
_.union(
checkedKeys.filter((item) => {
return (
item !== e.node.key &&
!e.node.children.filter((child) => child.key === item).length
);
})
)
);
} else {
setCheckedKeys(
_.cloneDeep(checkedKeys.filter((item) => item !== e.node.key))
);
}
}
},
[checkedKeys, setCheckedKeys]
);
const onSelect = (selectedKeysValue, info) => {
console.log("onSelect", info);
setSelectedKeys(selectedKeysValue);
};
React.useEffect(() => {
const checked = [];
treeData.forEach((data) => {
data.children.forEach((item) => {
if (item.checked) {
checked.push(item.key);
}
});
});
setCheckedKeys(checked);
}, []);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
React.useEffect(() => {
if (searchValue) {
const filteredData = filteredTreeData(
treeData,
searchValue,
checkedKeys,
setExpandedKeys
);
setTree([...filteredData]);
} else {
setTree(treeData);
// setExpandedKeys([]);
}
}, [searchValue, checkedKeys]);
return (
<div>
<Search
style={{ marginBottom: 8 }}
placeholder="Search"
onChange={(e) => {
setSearchValue(e.target.value);
}}
/>
<Tree
checkable
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onCheck={onCheck}
checkedKeys={checkedKeys}
onSelect={onSelect}
selectedKeys={selectedKeys}
treeData={tree}
/>
</div>
);
};
CodeSandBox Link
When you run the filterTreeData function, you assign the children to only the tree that contains the search term. So, you're searching n levels deep for the term. Notice that it works like I'd expect it to if you only search one level deep. That is, if the node has children, you return the node as such rather than filtering down all the children that don't also contain the term. when you filter with the string "new" in your example, you'll notice that none of the children in the collection "New Watchlists" contain the term and they are therefore filtered out leaving you with an empty array. My naive solution is to just return the children as in my following example:
function filteredTreeData(data, searchString, checkedKeys, setExpandedTree) {
let keysToExpand = [];
const filteredData = searchString
? filterData(data, searchString).map((n) => {
keysToExpand.push(n.key);
return {
...n,
children: n.children
};
})
: data;
setExpandedTree([...keysToExpand]);
return filteredData;
}
I call this solution naive, because I'm not exactly sure what your use case is. There might be more conditional logic you need to render in there for first searching n levels deep for the term and then and returning the whole node if the term is discovered anywhere inside the node or children.
On my current application, if a user tries to enter an existing name that has a different number, it will prompt the user if they want to update that entry with the new number. If yes, the entry is updated using an axios PUT request. My issue is that I can only get it to change on the front end by reloading the page (it updates successfully on db.json) instead of it updating immediately after the user confirms. On my useEffect method I tried adding [persons] as the second argument and it seemed to work, but found out that it loops the GET requests infinitely. I have a similar function for when deleting an entry so I'm sure it must be something that has to be added to setPersons
Update methods
const addEntry = (event) => {
event.preventDefault();
const newPersonEntry = {
name: newName,
number: newNumber,
}
const all_names = persons.map(person => person.name.toUpperCase())
const all_numbers = persons.map(person => person.number)
const updatedPerson = persons.find(p => p.name.toUpperCase() === newName.toUpperCase())
const newPerson = { ...updatedPerson, number: newNumber };
if (newName === '') {
alert('Name entry cannot be blank')
return
}
if (newNumber === '') {
alert('Number entry cannot be blank')
return
}
if (all_numbers.includes(newNumber)) {
alert('That number already exists')
return
}
if (newNumber.length < 14) {
alert('Enter a valid number')
return
}
if (all_names.includes(newName.toUpperCase())) {
if (window.confirm(`${newName} already exists, replace number with the new one?`)) {
console.log(`${newName}'s number updated`)
personService
.update(updatedPerson.id, newPerson)
.then(res => {
setPersons() //something here
})
return
}
return
}
personService
.create(newPersonEntry)
.then(person => {
setPersons(persons.concat(person))
setNewName('')
setNewNumber('')
})
}
//PUT exported as personService
const update = (id, newObject) => {
const request = axios.put(`${baseURL}/${id}`,newObject)
return request.then(response => response.data)
}
Other code
const App = () => {
const [persons, setPersons] = useState([])
useEffect(() => {
personService
.getAll()
.then(initialPersons => {
setPersons(initialPersons)
})
}, [])
...
//Display method
const filteredNames = persons.filter(person => person.name.toLowerCase().includes(filter.toLowerCase()))
const row_names = () => {
return (
filteredNames.map(person =>
<p key={person.id}>{person.name} {person.number} <button onClick={() => handleDelete(person)}>delete</button></p>));
}
...
//Render
return (
<div>
<h2>Phonebook</h2>
<h2>Search</h2>
<SearchFilter value={filter} onChange={handleFilterChange} />
<h2>Add Entry</h2>
<Form onSubmit={addEntry}
name={{ value: newName, onChange: handleNameChange }}
number={{ value: newNumber, onChange: handleNumberChange }}
/>
<h2>Numbers</h2>
<DisplayPersons persons={row_names()} />
</div>
)
}
The solution here is a little bit tricky but doable . You need to split your logic into two parts like this :
const [dataChanged , setDataChanged] = useState(false)
useEffect(()=>{
// Rest of your logic here
} , [dataChanged])
useEffect(()=>{
// Your logic will run only one time
// on Success we change the dataChanged state so the other useEffect will
// run basically you can run the rest of your logic in the other
// useEffect so the infinite loop won't happen
// setDataChanged( (prev) => !prev )
} , [])
Was able to use map method that worked
personService
.update(updatedPerson.id, newPerson)
.then(res => {
setPersons(persons.map(p => p.id !== updatedPerson.id ? p : res))
})
I want to delete this TODO with or without using a unique key
this is the HOOK code
const [todos, setTodos] = useState([{}])
const [user, setUser] = useState({
id: uuidv4(),
name: '',
email: '',
phone: '',
})
This one is the Function to set Input and delete a todo
const addTodo = (e) => {
e.preventDefault()
setTodos([...todos, user])
console.log(addTodo)
}
console.log(user)
const delTodo = (e, id) => {
e.preventDefault()
console.log(id)
todos.splice(id, 1)
setTodos([...todos])
}
Here These are being mapped
{todos.map((todo) => (
<div>
<li key={todo.id}>
{todo.name}, {todo.email}, {todo.phone}
</li>
<button onClick={delTodo} color='danger'>
Delete
</button>
</div>
))}
This is what i get when i console.log
link to image
Update your delTodo function as below.
splice uses first parameter as start index from array that you want to remove and second parameter is deleteCount. So in your case you need to get index of your object.
You can get index of object with indexOf(). It will return -1 of object does not belong to that array. So add if (index != -1) { } and then you can use todos.splice(index, 1); inside it.
const delTodo = (e, id) => {
e.preventDefault();
console.log(id);
let index = todos.indexOf(id);
if (index != -1) {
todos.splice(index, 1);
}
setTodos([...todos]);
}
Let's say I have a todo list array of objects:
const todos = [
{
id: 1,
task: "Take out Trash",
completed: false,
},
{
id: 2,
task: "Make Dinner",
completed: false,
},
export default todos
How would you go about updating completed property to true in a functional component? I understand the code I have below is merely changing checked to true. I just need to figure out how to interact with the data source.
import ToDoData from "./data/tododata";
const myComp = () => {
const [isChecked, setIsChecked] = useState(false);
const handleChange = () => {
setIsChecked(true)
};
return (
<input
type="checkbox"
checked={isChecked}
onChange={props.handleChange}
/>
)
}
Your question is a bit unclear. if you want to keep the array of todos as state & toggle the completed property of one of its members, then you need to pass an id with you onChange function which would look like this:
const handleChange = (id) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
but the better approach would be mapping over TodoData & returning a Todo component for each like this:
TodoData.map(todo => <Todo todo={todo}/>)
then inside you Todo component you can have:
const Todo = () => {
const [todo, setTodo] = useState(todo)
const handleChange = () => {
setTodos((todo) => ({ ...todo, completed: !todo.completed }));
};
return (
<input type="checkbox" checked={isChecked} onChange={props.handleChange} />
);
};
this way you don't need to map over the whole array everytime one checkbox gets toggled
I am using react usestate() and I want to update device state(It is an object)
my problem is when ShowRelays component renders for the first time device is an empty object and It does not get updated during first rendering, but for the next renders everything is fine
How can I update device state for the first time rendering?
(sorry for my bad english)
.
function ShowRelays(props) {
const [device, setDevice] = useState({})
let reduxDevices = useSelector(state => state.devicesReducer.devices)
let findDevice = () => {
let myDevice = reduxDevices.find(x => x._id === props.id)
setDevice(prevState => {
return {
...prevState,
...myDevice
}
})
}
useEffect(() => {
if (props.show) {
findDevice()
}
}, [props.show])
return
(
<div>
test
</div>
)
}
myDevice object is like:
{active: true, name: "device1", id: "deviceId"}
You can pass a function to your useState-hook which will calculate your initial value for device.
see lazy init state
function ShowRelays(props) {
let reduxDevices = useSelector(state => state.devicesReducer.devices);
const [device, setDevice] = useState(() => {
return reduxDevices.find(x => x._id === props.id)
});
return <div>test</div>;
}
An other possible solution for your problem without using a separate state could be the following (directly select the right device from your selector function):
function ShowRelays(props) {
const device = useSelector(state => {
return state.devicesReducer.devices.find(x => x._id === props.id);
});
return <div>test</div>;
}