So, i have faced similar issues in the past, but could always figure out where i am missing to spread or assign a new object, while trying to update previous state. But i can't seem to figure out what i am doing wrong this time.
I have a state, in a functional component, that looks like this:
formData = {name: '', features: []}
And trying to update the features array with the following snippet:
function handleUpdate(featureUID) {
console.log('Called parent')
setFormData(prev => {
const spread = Object.assign({}, prev)
let features = [...spread.features]
if (features.includes(featureUID)) {
features = features.filter(x => x !== featureUID)
} else {
features = [...features, featureUID]
}
console.log('Features:', features)
spread.features = [...features]
return spread
})
}
So as you can see, i am initially assigning a shallow-copied version of prev to spread and further down, i also create a shallow copy of the features array.
But seems like after the first invocation of this method, on all consequent calls triggers the setState twice.
What am i missing here?
Edit 1
Forgot to mention the function that holds the setFormData aka setState is only being called once, confirmed via console logging. The way i am calling it:
{otherList?.map(single => <Button key={single.uid} onClick={() => handleUpdate(single.uid)}/>)}
Edit 2
I am aware of the fact that StrictMode causes the twice calling of setState. But i also know why it's that way. So not willing to remove StrictMode
Edit 3
Tried this and seems to work:
setFormData(prev => ({
...prev,
features: prev.features.includes(featureUID)
? prev.features.filter(x => x !== featureUID)
: [...prev.features, featureUID]
}))
But what i don't understand is, Why ? What did i miss in my first approach?
Related
Why this works
const handleToggle = (id) => {
const newTodos = [...todos]
newTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
});
setTodos(newTodos);
}
And this doesnt
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
}))
}
Why do i have to create a copy of the old todos array if i want to change some item inside it?
You are doing a copy of the array in both cases, and in both cases you are mutating the state directly which should be avoided. In the second version you also forgot to actually return the todo, so you will get an array of undefined.
Instead you should shallow copy the todo you want to update.
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
return {...todo, completed: !todo.completed}
}
return todo
}))
}
Why mutating state is not recommanded? source
Debugging: If you use console.log and don’t mutate state, your past
logs won’t get clobbered by the more recent state changes. So you can
clearly see how state has changed between renders.
Optimizations: Common React optimization strategies rely on skipping
work if previous props or state are the same as the next ones. If you
never mutate state, it is very fast to check whether there were any
changes. If prevObj === obj, you can be sure that nothing could have
changed inside of it.
New Features: The new React features we’re
building rely on state being treated like a snapshot. If you’re
mutating past versions of state, that may prevent you from using the
new features.
Requirement Changes: Some application features, like implementing
Undo/Redo, showing a history of changes, or letting the user reset a
form to earlier values, are easier to do when nothing is mutated. This
is because you can keep past copies of state in memory, and reuse them
when appropriate. If you start with a mutative approach, features like
this can be difficult to add later on.
Simpler Implementation: Because
React does not rely on mutation, it does not need to do anything
special with your objects. It does not need to hijack their
properties, always wrap them into Proxies, or do other work at
initialization as many “reactive” solutions do. This is also why React
lets you put any object into state—no matter how large—without
additional performance or correctness pitfalls.
In practice, you can often “get away” with mutating state in React,
but we strongly advise you not to do that so that you can use new
React features developed with this approach in mind. Future
contributors and perhaps even your future self will thank you!
when you're changing an object using a hook correctly (- in this case useState) you are causing a re-render of the component.
if you are changing the object directly through direct access to the value itself and not the setter function - the component will not re-render and it will likely cause a bug.
and the .map(function(...)=>{..}) is a supposed to return an array by the items that you are returning from the function within. since you're not returning anything in the second example - each item of the array will be undefined - hence you'll have an array of the same length and all the items within will be undefined.
these kinds of bugs will not happen if you remember how to use array functions and react hooks correctly,
it's usually really small things that will make you waste hours on end,
I'd really recommend reading the documentation.
I am currently following a React course on Scrimba on creating a web app for taking notes.
The problem requires me to bump a note to the top of the note list every time it's updated.
The notes are initialised through useState as follows:
const [notes, setNotes] = useState([])
The array consists of the individual notes as objects with an id and body
Every time an onChange is triggered, the following function is ran:
function updateNote(text) {
setNotes(oldNotes => {
let updated = oldNotes.map(oldNote => {
return oldNote.id === currentNoteId
? { ...oldNote, body: text }
: oldNote
})
const currNoteIndex = updated.findIndex(
note => note.id === currentNoteId
)
console.log(currNoteIndex)
updated.unshift(updated.splice(currNoteIndex, 1))
return updated
})
}
However, I keep getting an error as shown in the image.
It's very unclear to me where the problem lies, but I'm thinking it has to do with the array methods.
Any explanation for this issue would be greatly appreciated!
Credits to jsN00b for the answer:
array.splice returns an array, not the object.
Since that array is inserted to the start of the array containing the objects, there will be an error when updateNote() is called again.
I don't know exactly what it is, but I have run into countless problems in trying to do the simplest state updates on arrays using hooks.
The only thing that I have found to work is using the useReducer to perform a single update on the array with putting dispatch on onClick handlers. In my current project, I am trying to update array state in a for loop nested in a function that runs on a form submit. I have tried many different solutions, and this is just one of my attempts.
function sessionToState(session) {
let formattedArray = []
for (let i = 0; i < session.length; i++) {
formattedArray.push({ url: session[i] })
setLinksArray([...linksArray, formattedArray[i]])
}
}
// --------------------------------------------------------
return (
<div>
<form
method="post"
onSubmit={async e => {
e.preventDefault()
const session = await getURLs({ populate: true })
sessionToState(session)
await createGroup()
I was wondering if there are any big things that I am missing, or maybe some great tips and tricks on how to work with arrays using hooks. If any more information is needed don't hesitate to ask. Thanks.
I was wondering if there are any big things that I am missing
TLDR: setLinksArray does not update linksArray in the current render, but in the next render.
Assuming the variables are initialized as follows:
const [linksArray, setLinksArray] = useState([])
A hint is in the const keyword, linksArray is a constant within 1 render (and this fact wouldn't change with let, because it's just how useState works).
The idea of setLinksArray() is to make a different constant value in the next render.
So the for loop would be similar to:
setLinksArray([...[], session0])
setLinksArray([...[], session1])
setLinksArray([...[], session2])
and you would get linksArray = [session2] in the next render.
Best way to keep sane would be to call any setState function only once per state per render (you can have multiple states though), smallest change to your code:
function sessionToState(session) {
let formattedArray = []
for (let i = 0; i < session.length; i++) {
formattedArray.push({ url: session[i] })
}
setLinksArray(formattedArray)
}
Furthermore, if you need to perform a side effect (like an API call) after all setState functions do their jobs, i.e. after the NEXT render, you would need useEffect:
useEffect(() => {
...do something with updated linksArray...
}, [linksArray])
For a deep dive, see https://overreacted.io/react-as-a-ui-runtime
When invoking state setter from nested function calls you should use functional update form of setState. In your case it would be:
setLinksArray(linksArray => [...linksArray, formattedArray[i]])
It is not exactly clear what kind of problems you encounter, but the fix above will save you from unexpected state of linksArray.
Also this applies to any state, not only arrays.
Performance wise you shouldn't call setState every iteration. You should set state with final array.
const sessionToState = (session) => {
setLinksArray(
session.map(sessionItem => ({url: sessionItem}))
);
}
... or if you want to keep old items too you should do it with function inside setState ...
const sessionToState = (session) => {
setLinksArray(oldState => [
...oldState,
...session.map(sessionItem => ({url: sessionItem}))
]);
}
I'm calling console.log as a callback from setState and it seems like setState doesn't work. I'm trying to write a function which takes state defined like this:
this.state = {
...this.props.substate
};
this.props.substate is an array of objects, so I'm transforming it to object.
Here is a function that transforms state back to array, removes last element and then transforms array into object:
handleClickRem = event => {
event.preventDefault();
console.log(this.state);
const arrayFromState = Object.values(this.state);
console.log(arrayFromState);
const newArray = arrayFromState.slice(0, -1);
console.log('newArray',newArray);
const newState = {...newArray};
console.log(newState);
//newState is exactly what I want it to be
this.setState(newState, () => console.log(this.state));
};
All console logs works perfectly apart from the last one. I'm also checking state in render(), but it also seems to show no change of state.
I've done similarly a function, which adds elements to state and it works perfectly.
I'll be very thankful for help and patience :)
I'm studying React JS making a simple todo list and part of my code is as follows:
changeStatus(e, index) {
this.state.tasks[index].status = e.target.value;
this.setState((prevState) => ({
tasks: prevState.tasks
}));
}
As React documentation says, we shouldn't change state manually, we should call setState instead, so, I'm receiving a warning on console saying that, even calling setState after the manual changing.
I'm doing so because, for me, is more practical doing that than separating the task, doing a splice on my array, and calling setState with the new values.
Is there a problem with my approach? How would you recommend updating state in a efficient and non-repetitive way?
Try this :
changeStatus(e, index) {
const tmp = { ...this.state };
tmp.tasks[index].status = e.target.value;
this.setState(tmp);
}
You should not mutate the state directly, to do that create a new object from the state and then update that and set it back to state like
changeStatus(e, index) {
var tasks = {...this.state.tasks}
tasks[index].status = e.target.value;
this.setState({tasks});
}
In case you are wondering as to what {...this.state.tasks} do then check this answer:
What is the meaning of this syntax "{...x}" in Reactjs
However you can also make a copy of the state using the ES5 Object.assign() operator like
var tasks = Object.assign({}, this.state.tasks)