useState not updating an array correctly - reactjs

I'm using React with Firebase to fetch data from firestore and display it to the user. Within a useEffect hook, I fetch the data and add it to an array of json objects. However, when adding a new element to the array, the previous one gets deleted. Below will be relevant portions of code.
const [items, setItems] = useState([]);
const [isLoading, setLoading] = useState(true);
const { currentUser } = useAuth();
function addItemToList(i) {
const updatedList = [
...items,
{
data: i.data(),
ref: i.ref
}
]
return updatedList;
}
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
itemSnapshot.forEach((doc) => {
setItems(addItemToList(doc));
})
})
setLoading(false);
}, []);
I've also tried updating the items list within the useEffect hook, but it produces the same bug.

Let me take a moment to explain what is happening here.
Your have to think in your react component as a "Snapshot", where a snapshot is the result of a render. Each snapshot points to a state created by useState and only to that one. What does this mean?
The first time your component renders (first snapshot) the items variable returned by useState is pointing to an empty array and as long as that snapshot is alive it would be pointing to that array.
So, lets take a look to the function called by useEffect
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
itemSnapshot.forEach((doc) => {
setItems(addItemToList(doc));
})
})
setLoading(false);
you are calling the set function for items once per elemtent in firebase query result and each call is seting a value that is the result of calling addItemToList
function addItemToList(i) {
const updatedList = [
...items,
{
data: i.data(),
ref: i.ref
}
]
return updatedList;
}
the point here is that you are always destructuring ...items which is always the items variable that the snapshot of the component is pointing to and it would not be updated until the component re rederds.
So, basically you all aways doing updatedList = [ ...[], { ... } ] becauses items has to wait to take update his value.
#Viet already responded with something good, which I think it's the best option
mapping the result of the query to an array and updating the array with the result of the map a parameter.
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
const result = [];
itemSnapshot.forEach((doc) => {
result.push({
data: i.data(),
ref: i.ref
});
})
setItems(result);
})
setLoading(false);
}, []);

You need to add "addItemToList" to the dependencies of the useEffect hook.
Because you haven't done this, the effect references an old version of that function which references an old version of the items array.

You just use itemSnapshot.map like this:
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
setItems(
itemSnapshot.map((doc) => ({
data: i.data(),
ref: i.ref,
})),
);
})

Instead of running it for each value maybe you should try to map it as a list and update the state once
function addItemsToList(newItems) {
const updatedList = [
...items,
...newItems
]
return updatedList;
}
useEffect(() => {
firestore.collection('items').where('uid', '==', currentUser.uid).get().then((itemSnapshot) => {
const docs = itemSnapshot.map((doc) => ({
data: doc.data(),
ref: doc.ref
});
setItems(addItemsToList(docs));
})
setLoading(false);
}, []);

You are using spread operator in a wrong way! This way will not update the array.
You can do that by using es6 spread to concat multiple arrays something like below:
const updatedList = [
...items,
...[{ data: i.data(), ref: i.ref }]
]
To know more: Using es6 spread to concat multiple arrays

Related

react useEffect strange behaviour - can't explain?

I have the following code in a useEffect
useEffect(() => {
async function fetchMessages() {
let messages = [];
const firestore = firebase.firestore();
const query = firestore.collection('chats').where("repliedTo", "==", false).where("type", "==", "StudentQuery").orderBy("timestamp", "desc");
query.onSnapshot({
next: (querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log("x ", doc.id, '=>', doc.data());
messages.push({mid: doc.id, ...doc.data()});
console.log(messages)
});
},
});
setMessagesList(messages)
setMessageCount(messagesList.length)
console.log('xxxx' + messagesList.length)
}
fetchMessages();
}, [messagesList.length]);
A few things seem to be wrong with this and I can't see it.
When I trigger this code (by inserting a new record into Firestore) what I expect would be to see a console.log with the (final) array size (so previous array size + 1) - but instead what I am seeing is the previous array + (previous array + 1 entry) I would have thought he let messages = [] would have cleared the array every time an update happened?
I never see the console.log("xxx") in my console. I want to put a state update here as this line should be safe as the database read has done, but since the line doesn't appear I don't know what's going wrong.
Can anyone shed some insight?
I've not used firebase before but it looks like you're effectively creating a subscription which is getting called outside of React's render cycle.
You could just add a state property that you update when next is called, eg:
const [messages, setMessages] = useState([]);
// Use the `useEffect` to set up / tear down the subscription
useEffect(() => {
const firestore = firebase.firestore();
const query = firestore.collection(...);
const unsubscribe = query.onSnapshot({
next: (querySnapshot) => {
setMessages(prev => [
...prev,
...querySnapshot.map(doc => ({
mid: doc.id,
...doc.data(),
})),
]);
});
});
// Unsubscribe when you unmount
return () => unsubscribe();
}, [])

Multiple useLazyQuery hooks (Apollo Client) in React Function Component

I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes

Axios and looped promises

I have problem with loop on axis GET request, and I can't understood why.
const [ state, setState ] = useState<any[]>([]);
ids.forEach((id) => {
getData(id)
.then((smth: Map<string, any>[]) => getNeededData(smth, id));
});
console.log(JSON.stringify(state));
and getData (getNeededData is only choose parameters):
export const getData= async (id: string) => {
const response = await Axios.get(`/rest/${id}`)
.then((res: { data: any; }) => res.data);
return response;
};
I should have 2 response (it's 2 id in variable "ids"), but I have first, second, first, second, first, and this in a loop.
Why it's been working like this?
What I can change for fix this?
By putting that forEach at the top level of your component function, you're running it every time the function is called by React to render its contents, which React does when state changes. The code you've shown doesn't set state, but I'm assuming your real code does.
To do it only when the component first mounts, wrap it in a useEffect callback with an empty dependency array:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
ids.forEach((id) => {
getData(id)
.then(/*...*/);
});
}, []);
If all of the results are going in the state array, you probably want to use map and Promise.all to gether them all up and do a single state change with them, for instance:
const [ state, setState ] = useState<any[]>([]);
useEffect(() => {
Promise.all(
ids.map((id) => {
return getData(id).then(/*...*/);
})
)
.then(allResults => {
// Use `allResults` to set state; it will be an array in the same order
// that the `id` array was in
})
.catch(error => {
// handle/report error
});
}, []);

Run Apollo mutation on first render with useEffect

I have a form in a MUI dialogue. I want to create an order when the dialogue first opens. I thought useEffect with empty dependency would do that but I can't seem to figure out why it doesn't let the mutation resolve before the setState.
const [createOrder] = useMutation(CREATE_ORDER);
const [activeOrder, setActiveOrder] = useState({});
useEffect(() => {
const newOrder = async () => {
await createOrder({
variables: {
order: {
type,
table,
},
},
refetchQueries: [{ query: ORDERS_QUERY }],
});
};
setActiveOrder(newOrder);
}, []);
console.log(activeOrder); // gives me Promise{<fulfilled>: undefined}
Also, is the only way to get rid of the missing dependency warning with // eslint-disable-next-line? If I wrap my function in a useCallback to prevent render loop and add the dependencies requested, then useCallback gives me an error that the dependencies are unknown.
your problem is here setActiveOrder(newOrder);
setActiveOrder can accept function as an argument and call it;
newOrder is async function. async function always returns promise;
solution:
/*
maybe your don't need activeOrder, check data from result
https://www.apollographql.com/docs/react/data/mutations/#result
*/
const [createOrder, { data }] = useMutation(CREATE_ORDER, {
// https://www.apollographql.com/docs/react/data/mutations/#options
variables: { type, table },
refetchQueries: [{ query: ORDERS_QUERY }],
});
const [activeOrder, setActiveOrder] = useState({});
useEffect(() => {
createOrder().then((res) => {
setActiveOrder(res);
});
}, [createOrder]);
useEffect(() => console.log('data:\n', data), [data]);
useEffect(() => console.log('activeOrder:\n', activeOrder), [activeOrder]);

cannot set State in functional component while fetching data from Firebase in reactJS

useEffect(() => {
console.log("mounted");
all.db.ref("NewDonor").on("child_added", (data) => {
var DataFromDB = data.val();
state.donor.push(DataFromDB);
console.log(state.donor);
setState({
donor: DataFromDB,
});
});
}, [0]);
Here is some of my code.. i want to fetch data from firebase and add into my state and render on DOM,
but cannot be able to set state..it's showing an error,
state.donor.map is not a function
Assuming your state is an array
const [state, setState] = useState({ donor: [] });
Then this is how you would update it. Spread any existing state into a new array, and then also spread in the new data from the DB. This ensures that state.donor remains an array for mapping later. I also don't think [0] is a valid dependency array, seems you want to only trigger effect when the component mounts so an empty dependency array works.
useEffect(() => {
all.db.ref("NewDonor").on("child_added", (data) => {
const DataFromDB = data.val();
setState(prevState => ({
donor: [...prevState, ...DataFromDB],
}));
});
}, []);

Resources