Updating an object inside an array in Firebase Firestore react JS - arrays

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();
}

Related

How could I write this function so it doesn't setState within the foreach everytime

The function collects role Assignment PrincipalIds on an item in SPO. I then use a foreach to populate state with the Title's of these PrincipalIds. This all works fine but it's inefficient and I'm sure there is a better way to do it than rendering multiple times.
private _permJeChange = async () => {
if(this.state.userNames){
this.setState({
userNames: []
});
}
var theId = this.state.SelPermJEDD;
var theId2 = theId.replace('JE','');
var info = await sp.web.lists.getByTitle('MyList').items.getById(theId2).roleAssignments();
console.log(info, 'info');
var newArr = info.map(a => a.PrincipalId);
console.log(newArr, 'newArr');
// const userIds = [];
// const userNames = [];
// const userNameState = this.state.userNames;
newArr.forEach(async el => {
try {
await sp.web.siteUsers.getById(el).get().then(u => {
this.setState(prevState => ({
userNames: [...prevState.userNames, u.Title]
}));
// userNames.push(u.Title);
// userIds.push(el);
});
} catch (err) {
console.error("This JEForm contains a group");
}
});
}
I've left old code in there to give you an idea of what I've tried. I initially tried using a local variable array const userNames = [] but declaring it locally or even globally would clear the array everytime the array was populated! So that was no good.
PS. The reason there is a try catch is to handle any SPO item that has a permissions group assigned to it. The RoleAssignments() request can't handle groups, only users.
Create an array of Promises and await them all to resolve and then do a single state update.
const requests = info.map(({ PrincipalId }) =>
sp.web.siteUsers.getById(PrincipalId).get().then(u => u.Title)
);
try {
const titles = await Promise.all(requests);
this.setState(prevState => ({
userNames: prevState.userNames.concat(titles),
}));
} catch (err) {
console.error("This JEForm contains a group");
}

How to pass an array in query parameter url react

I Am using multiselect dropdown, what i want is whatever i've selected in dropdown to send it to the server by calling an api which contains query param to accomodate these dropdown result. I have made an array of selected items.
Array(3) [ "contact", "fee", "inbox" ]
I want this array to get pass to below url like this:
http://localhost.com/api/influencers?status=contact&status=fee&status=inbox
With my approach i am ending up with this:
http://localhost:8080/details?status[]=contact&status[]=fee&status[]=inbox
Can anyone please help me with it
const InfluencersList = props => {
const [availability, setAvailability] = useState(null);
const handleAvailabilityChange = value => {
const availability1 = value;
setAvailability(value);
getFilterData(availability1, null);
};
const getFilterData = (search, pageNumber) => {
let params = {};
params.status = search; //search is array [contact,
if (pageNumber) {
params.page = pageNumber; // no is array is number
}
axios.get("/api/influencers", { params: params }).then(res => {
setState({
items: res.data.data
});
});
};
<ChoiceList
title="Availability"
titleHidden
choices={statuses}
selected={availability || []}
onChange={handleAvailabilityChange}
allowMultiple
/>
}
you would need to use URLSearchParams to build your params with multiple query with same name.
Iterate over search and for each value you append to your params a new status value:
const getFilterData = (search, pageNumber) => {
const params = new URLSearchParams();
search.forEach(value => params.append('status', value));
if (pageNumber) {
params.append('page', pageNumber) ;
}
axios.get("/api/influencers", { params }).then(res => {
setState({
items: res.data.data
});
});
};
Why not -with axios- you send a string as:
"contact;fee;inbox"
and in the backend convert this string to an array using a function like split(';')?

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);
}

How to update state in react.js?

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
})
}

Resources