How to update state in react.js? - reactjs

I'd like to ask if there is any better way to update state in react.js.
I wrote this code below but just updating state takes many steps and I wonder if I'm doing in a right way.
Any suggestion?
How about immutable.js? I know the name of it but I've never used it and don't know much about it.
code
toggleTodoStatus(todoId) {
const todosListForUpdate = [...this.state.todos];
const indexForUpdate = this.state.todos.findIndex((todo) => {
return todo.id === todoId;
});
const todoForUpdate = todosListForUpdate[indexForUpdate];
todoForUpdate.isDone = !todoForUpdate.isDone;
this.setState({
todos: [...todosListForUpdate.slice(0, indexForUpdate), todoForUpdate, ...todosListForUpdate.slice(indexForUpdate + 1)]
})
}

You are using an extra step that you don't need. Directly setting value to the cloned object and restting back to state will work
toggleTodoStatus(todoId) {
const todosListForUpdate = [...this.state.todos];
const indexForUpdate = this.state.todos.findIndex((todo) => {
return todo.id === todoId;
});
todosListForUpdate[indexForUpdate].isDone = !todosListForUpdate[indexForUpdate].isDone;
this.setState({
todos: todosListForUpdate
})
}

Related

Updating an object inside an array in Firebase Firestore react JS

I want to be able to update a string inside an array that is stored in Firestore.
Now, I went througn their docs, and there is no such method. what they do provide is arrayUnion (to add another element to the array) and arrayRemove (to remove an element from an array).
So I thoguht I call on arrayUnion to add the new content and then arrayRemove to remove the old one thus, in practice, updating it.
However, if I only use arrayUnion it works fine, if I use both, only arrayRemove works and the new elemnt is not added. any Ideas?
const updateField = async (e, id, obj) => {
const taskDoc = doc(db, "Task", id);
if (e.target.id == "updateTodos") {
const updatedTask = {
Todos: arrayUnion(updatedTodo),
Todos: arrayRemove(obj),
};
await updateDoc(taskDoc, updatedTask);
setUpdateHadHappened(updateHasHappened + 1);
exitEditMode();
}
notice that writing:
Todos: arrayUnion(updatedTodo), arrayRemove(obj),
or
Todos: arrayUnion(updatedTodo); arrayRemove(obj);
does not work..
ok, solved it! it's all about syntax my friends:
const updateField = async (e, id, obj) => {
const taskDoc = doc(db, "Task", id);
if (e.target.id == "updateTodos") {
const updatedTask = {
Todos: arrayUnion(updatedTodo),
};
await updateDoc(taskDoc, updatedTask);
const updatedTask2 = {
Todos: arrayRemove(obj),
};
await updateDoc(taskDoc, updatedTask2);
setUpdateHadHappened(updateHasHappened + 1);
exitEditMode();
}

Firestore: calling collections.get() inside promise()

useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().

How to handle array state filter clashes

I am currently having an issue where multiple setStates that use the filtering of an array are interfering with each other. Basically if a user uploads two files, and they complete around the same time, one of the incomplete files may fail to be filtered from the array.
My best guess is that this is happening because they are separately filtering out the one that needs to be filtered, when the second one finishes and goes to filter itself out of the array, it still has the copy of the old incomplete array where the first file has not been filtered out yet. What would be a better way to approach this? Am I missing something obvious? I am thinking of using an object to hold the files instead, but then I would need to create a custom mapping function for the rendering part so that it can still be rendered as if were an array.
fileHandler = (index, event) =>{
let incompleteFiles = this.state.incompleteFiles
incompleteFiles[index].loading = true
incompleteFiles[index].file = event.target.files[0]
this.setState({ incompleteFiles: incompleteFiles },()=>{
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
let incompleteFiles = this.state.incompleteFiles
let completeFiles = this.state.completeFiles
api.uploadFile(fileData)
.then(res=>{
if(res.data.success){
this.setState(state=>{
let completeFile = {
name : res.data.file.name,
}
completeFiles.push(completeFile)
incompleteFiles = incompleteFiles.filter(inc=>inc.label !== res.data.file.name)
return{
completeFiles,
incompleteFiles
}
})
}
})
})
}
Updated with accepted answer with a minor tweak
fileHandler = (index, event) =>{
this.setState(({ incompleteFiles }) => ({
// Update the state in an immutable way.
incompleteFiles: [
...incompleteFiles.slice(0, index),
{
...incompleteFiles[index],
loading: true,
file: event.target.files[0],
},
...incompleteFiles.slice(index+1)
],
}), () => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
api.uploadFile(fileData)
.then(res => {
if(res.data.success){
this.setState(({ incompleteFiles, completeFiles }) => ({
completeFiles: [
...completeFiles, // Again, avoiding the .push since it mutates the array.
{ // The new file.
name: res.data.file.name,
}
],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name),
})))
}
})
});
}
In class components in React, when setting the state which is derived from the current state, you should always pass a "state updater" function instead of just giving it an object of state to update.
// Bad
this.setState({ counter: this.state.counter + 1 });
// Good
this.setState((currentState) => ({ counter: currentState.counter + 1 }));
This ensures that you are getting the most up-to-date version of the state. The fact that this is needed is a side-effect of how React pools state updates under the hood (which makes it more performant).
I think if you were to re-write your code to make use of this pattern, it would be something like this:
fileHandler = (index, event) =>{
this.setState(({ incompleteFiles }) => ({
// Update the state in an immutable way.
incompleteFiles: {
[index]: {
...incompleteFiles[index],
loading: true,
file: event.target.files[0],
},
},
}), () => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
api.uploadFile(fileData)
.then(res => {
if(res.data.success){
this.setState(({ incompleteFiles, completeFiles }) => ({
completeFiles: [
...completeFiles, // Again, avoiding the .push since it mutates the array.
{ // The new file.
name: res.data.file.name,
}
],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name),
})))
}
})
});
}
Another thing to keep in mind is to avoid mutating your state objects. Methods like Array.push will mutate the array in-place, which can cause issues and headaches.
I think change code to this can solve your problem and make code easy to read.
fileHandler = async (index, event) =>{
const incompleteFiles = [...this.state.incompleteFiles]
incompleteFiles[index].loading = true
incompleteFiles[index].file = event.target.files[0]
this.setState(
{
incompleteFiles
},
async (prev) => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
const res = await api.uploadFile(fileData)
/// set loading state to false
incompleteFiles[index].loading = false
if (!res.data.success) {
return { ...prev, incompleteFiles }
}
// add new file name into completeFiles and remove uploaded file name from incompleteFiles
return {
...prev,
completeFiles: [...prev.completeFiles, { name : res.data.file.name }],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name)
}
})
)
}

How to Loop an Array Using Map Function Based on Condition and Then setState?

I want to have same functionality using map function. Following is my code:
async componentDidMount() {
const data = await TodoAPI.getTodo();
for(var i=0;i<data.response.length;i++)
{
if(data.response[i]._id === this.props.match.params.id)
{
this.setState({
todo_description: data.response[i].todo_description,
todo_responsible: data.response[i].todo_responsible,
todo_priority: data.response[i].todo_priority,
todo_completed: data.response[i].todo_completed
})
}
}
}
I guess you’re a bit confused, as map() would not be useful in this case. It creates a new array but what you want (correct me if I’m wrong) is to find the item, so find() seems to be what you need:
async componentDidMount() {
const data = await TodoAPI.getTodo();
const id = this.props.match.params.id;
const item = data.response.find(item => item._id === id);
this.setState(item);
}

Updating state in react

I couldn't get the idea of updating state in react.
state = {
products : []
};
handleProductUpVote = (productId) => {
const nextProducts = this.state.products.map((product)
=> {
if (product.id === productId) {
return Object.assign({}, product, {
votes: product.votes + 1,
});
} else {
return product;
}
});
this.setState({
products: nextProducts,
});
}
Why we need to clone the object? Can we simply write
if (product.id === productId) {
product.votes =
product.votes + 1;
});
State updates can happen asynchronously, and delegating actual change of the state helps to ensure that things won't go wrong. Passing a new Object with the change also lets react make a (shallow) check to see which parts of the state were updated, and which components relying on it should be rerendered.

Resources