React prevState parameter with map Function in setState - reactjs

I am want to implement toggle functionality. (I have always used prevState to perform toggle operation) But in this case if I use prevState within map function. It is displaying error that "prevState is undefined".
I am posting 2 code, first one is working without prevState...but I want to implement it using prevState as shown in example enter code here2 (but 2nd code is giving the above mentioned error)
Correct:
this.setState({
rawData: this.state.rawData.map(a => {
if(a.id === id){
a.completed = !a.completed
}
return a
})
})
example 2:
this.setState((prevState)=>{
rawData: this.state.rawData.map(a => {
if(a.id === id){
a.completed = !prevState.a.completed
}
return a
})
})

I think the mapping is incorrect from the second example because you aren't mapping from the prevState. prevState.a is likely what is undefined. You need to also shallow copy the element being updated, otherwise a.completed = !a.completed is considered a state mutation.
this.setState(prevState => ({
rawData: prevState.rawData.map(a => {
if (a.id === id) {
return {
...a,
completed: !a.completed,
}
}
return a
})
}))

this.setState((prevState)=>({
rawData: prevState.rawData.map(a => {
if(a.id === id){
a.completed = !a.completed
}
return a
})
}))
Try this.

Related

React State returning old value after waiting for update with Effect hook?

I'm trying to do something that in my mind is very simple.
I have an array of documents (firebase firestore) and I have a method to fetch From Document with timeStamp A to docuemnt with timeStamp B
In the fetch function that tries to see if the ref id has already been fetched, but the messages inside the fetchUpTo function never updates. While the one I log in the effect hook, updates as expected.
The top Log is the Inside fetchUpTo and the bottom one is the effect one.
The logs are from trying to refetch one of the documents present in the bottom log.
const fetchUpTo = (ref: any) => {
if (isFetching || isAtEnd) {
return
};
if (!messagesSnapShot) {
return;
}
if (messagesSnapShot.size < queryLimit) {
return;
}
let index = messages.findIndex(d => d.id === ref.id)
if (index !== -1) {
if (messageRefs.current[index] !== null) {
scrollToMessage(messageRefs.current[index])
return;
}
}
setIsFetching(true)
const lastVisible = messages[0]
const cQuery = query(collection(fireStore, "scab-chat", chatId, "messages"), orderBy('time', 'desc'), startAfter(lastVisible.data.time), endAt(ref.data.time));
getDocs(cQuery).then(newDocs => {
if (newDocs.size < queryLimit) {
setIsAtEnd(true)
}
const newD = newDocs.docs.map(doc => ({
data: doc.data(),
id: doc.id,
ref: doc
}));
setMessages(s => {
const f = newD.filter(doc => s.findIndex(d => d.id === doc.id) === -1)
return [...s, ...f]
})
})
}
After doing this, I "wait" for the state to update with an Effect hook
useEffect(() => {
if (messages) {
setIsFetching(false)
}
}, [messages])
The problem is I have this small part of the code
let index = messages.findIndex(d => d.id === ref.id)
if (index !== -1) {
if (messageRefs.current[index] !== null) {
scrollToMessage(messageRefs.current[index])
return;
}
}
React state will only rerender you app when the function finishes, so you will only check for the updated messages when you call fetchUpTo again. If you need the updated value on the same function call, try using flushSync.
There is a nice video with Dan Abramov trying to achieve the same as you, I will leave it here for reference: https://youtu.be/uqII0AOW1NM?t=2102
Okay so I fixed it kinda, I had to render a diferent component while the isFetchingState was true so
if(isFetching){return <Loader/>}
and then it worked. I still don't really understand why It didn't work in the first place.

How to use "if" inside useState prevState map

Does anybody know how can i use if statement like this.
This example doesnt work
uppy.on('complete', (result) => {
result.successful.forEach((file) =>
setImgs((prevState) =>
prevState.map((item) => {
if(item.id === file.id) {
return {
...item,
image: file.preview
}
}
})
)
)
})
And this works, but there s no if
uppy.on('complete', (result) => {
result.successful.forEach((file) =>
setImgs((prevState) =>
prevState.map((item) => ({
...item,
image: file.preview,
}))
)
)
})
I don't think you need to map if you're just trying to find an item.
You could do
const item = prevState.find(x.id ==> file.id)
return item? {...item.image:file.preview} : null
"doesn't work" will need more specification. Out of observation I can tell that it needed to have else statement or without, in order to return item if no change is required. The variable - item is unchanged element of imgs array, which we put back.
This is after refactoring your pseudocode:
uppy.on("complete", (result) => {
result.successful.forEach((file) =>
setImgs((prevState) =>
prevState.map((item) => {
if (item.id === file.id) {
return { id: item.id, image: file.preview };
} else return item;
})
)
);
});
Check the sandbox here
Since you are using a map that returns a new array, also you are trying to add an image key to the matched item only then, you need to also return for the else case.
const data = state.map((item) => {
if (item.id === file.id) return { ...item, image: file.preview };
return item;
});

Using spread operator to avoid mutating state in React function

In my React project I have a function within a class-based component that handles video uploads. It is working as expected and desired. However, I realized upon inspection that it violates React's don't mutate state mandate. I think that's the case, though I want to ensure that's true, and that the solution I've come up with deals with this.
Here is my component state:
state = {
streamingRes: {},
uploadFailed: false
}
My initial function looked like this (notice there are 3 places where I am setting the state):
fileUploadHandler = (id, file, fileId) => {
const isValid = this.validateVideoFileType(file);
if(!isValid) this.props.showError(`${file.name} is of the wrong file type (${file.type}). File must be an acceptable video format.`);
let dataStream = io.Stream.createStream();
io.Socket.on('userVideos.uploadProgress', (data) => {
this.setState( { streamingRes: data });
if(fileId === data.data.guid) {
this.uploadCompletionPercentage = data.data.progress;
}
});
io.Stream(io.Socket).emit('userVideos.upload', dataStream, {
guid: fileId,
size: file.size
}, (data) => {
if(data.status === "failure") {
this.props.onUploadFailed();
this.setState( { uploadFailed: true })
}
else if(data.status === "success") {
this.props.upload(id)
}
});
this.setState( { uploadFailed: false });
io.Stream.createBlobReadStream(file).pipe(dataStream);
return;
}
To avoid mutating state I updated this function to look like this:
handleFileUpload = (id, file, fileId) => {
let newState = {...this.state};
const isValid = this.validateVideoFileType(file);
if(!isValid) this.props.showError(`${file.name} is of the wrong file type (${file.type}). File must be an acceptable video format.`);
let dataStream = io.Stream.createStream();
io.Socket.on('userVideos.uploadProgress', (data) => {
this.setState( { streamingRes: data });
if(fileId === data.data.guid) {
this.uploadCompletionPercentage = data.data.progress;
}
});
io.Stream(io.Socket).emit('userVideos.upload', dataStream, {
guid: fileId,
size: file.size
}, (data) => {
if(data.status === "failure") {
this.props.onUploadFailed();
newState.uploadFailed = true;
this.setState( { uploadFailed: newState.uploadFailed });
}
else if(data.status === "success") {
this.props.upload(id)
}
});
newState.uploadFailed = false;
this.setState( { uploadFailed: newState.uploadFailed });
io.Stream.createBlobReadStream(file).pipe(dataStream);
return;
}
Notice I am using the spread operator right at the top of the function now. My question is: does this effectively deal with the issue of avoiding state mutation?
Yes, you have avoided mutating state. However, your way of doing it is completely unnecessary because there is no need to copy the state into a new object if you don't use that object.
Instead of:
newState.uploadFailed = true;
this.setState( { uploadFailed: newState.uploadFailed });
You can simply do:
this.setState({ uploadFailed: false });
There was no problem in your code in the first place.

React setState opposite boolean in map function not changing state

I created a simple to-do list in ReactJS. It loads components for to-do items stored in a file "TodoData.js", data is stored as followed in that file:
const todosData = [
{
id:1,
text: "Take out the thrash",
completed: true
},
{
id:2,
text: "Grocery shopping",
completed: false
},
App.js uses a TodoItem.js component to render each to-do item with a map function. TodoItem.js uses conditional rendering:
if (props.item.completed == true) {
return (
<div className="todoclassDone">
<input type="checkbox"
onChange={ () => props.handleChange(props.item.id)}/>
<p className="lalatext"><del>{props.item.text}</del></p>
</div>
)
}
else { .... //same code as above but with other className.
Within App.js I use the TodoItem.js component to render each item in TodoData with a map function; if the data.completed = true background is green, else background is red.
Problem: However, the handleChange(id) function in App.js is not working properly. I loop through all objects in todosData; if the id is similar to the id of checkbox which the user clicked it should change to the opposite boolean value using todo.completed = !todo.completed However, when running this code nothing is happening. The handleChange function:
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id == id) {
todo.completed = !todo.completed;
}
return todo
})
Extra info: Above mentioned problem is especially weird because if I change the boolean value of the checkbox clicked by the user to either false or true it does work. This does not result in the desired behaviour because now I am only able to change the todo.completed once from false to true. ; In this case the handleChange function would look as follows:
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id == id) {
todo.completed = true;
}
return todo
})
Any help is highly appreciated, thanks in advance! :-)
Ciao, you could try to copy state on an array, modify array and set the state wht updated array. Something like:
handleChange(id) {
let result = this.state.todos;
result = result.map(todo => {
if (todo.id === id) todo.completed = !todo.completed;
return todo;
})
this.setState({todos: result})
}
You should return the new state in your setState callback, with the new state object.
Example todo component with relevant code:
class Todo extends Component {
state = {
todos: todosData,
}
handleChange(id) {
this.setState(prevState => {
const todos = prevState.todos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo;
});
return { todos };
}
}

React - remove nested array item using setState function call

I am trying to remove a (semi) deeply nested item from an array using setState but it doesn't seem to be working. My state is structured as follows:
state = {
currentSeries: null,
currentRowIndex: null,
rows: [
{
id: shortid.generate(),
nodes: [],
series: [], // array with item I want to remove
},
],
};
and my remove item call:
onRemoveModelElementClick = (rowId, modelElementId) => {
this.setState((prevState) => {
const index = prevState.rows.findIndex(x => x.id === rowId);
const series = prevState.rows[index].series.filter(s => s.id !== modelElementId);
return series;
});
};
I tried spreading the remaining state is several ways but it does not seem to update properly. I the rowId and modelElementId are correct and I can verify they do filter the correct item out. I am just having trouble on what to return. I know it is something simple but for the life of me I can't see it.
My recommendation would be to use .map to make things are bit easier to digest. You can then write it like so:
onRemoveModelElementClick = (rowId, modelElementId) => {
const updatedRowsState = this.state.rows.map(row => {
// this is not the row you're looking for so return the original row
if (row.id !== rowId) {
return row;
}
const filteredSeries = row.series.filter(s => s.id !== modelElementId);
return {
// spread properties (id, node, series)
...row,
// overwrite series with item filtered out
series: filteredSeries,
};
});
// since rest of the state doesn't change, we only need to update rows property
this.setState('rows', updatedRowsState);
}
Hope this helps and let me know if you have any questions.
I think the issue here is how your code uses setState. The setState function must return an object. Assuming your filtering functions are correct as you describe, return an object to update the state:
return { series };
setState documentation
Here is what I did to get it working in case it can help someone else:
onRemoveModelElementClick = (rowId, modelElementId) => {
this.setState((prevState) => {
const updatedRowState = prevState.rows.map((row) => {
if (row.id !== rowId) {
return row;
}
const filteredSeries = row.series.filter(s => s.id !== modelElementId);
return {
...row,
series: filteredSeries,
};
});
return {
rows: updatedRowState,
};
});
};
All credit to Dom for the great idea and logic!

Resources