I have a problem which I could not figure out. I have this function which calculates the total price:
const [totalPrice, setTotalPrice] = useState(0);
const calcTotalPrice = (itemss) => {
var sum = null;
itemss.forEach(function (item) {
sum += item?.Quantity * item?.Prijs;
});
setTotalPrice(sum);
console.log(totalPrice);
};
It works great, but when I update the quantity of a product the state does not update. Well, it does but then rolls back to it's previous value. Here is the output I am getting:
570
443
467
Which is very weird. The top value is the new value, the second is the old and I don't know where the third one is coming from. The function is called everytime the function getItems() is called:
async function getItems() {
setLoading(true);
db.collection(`rooms/${roomId}/Items`).onSnapshot((querySnapshot) => {
const itemss = [];
querySnapshot.forEach((doc) => {
itemss.push(doc.data());
});
setItems(itemss);
calcTotalPrice(itemss);
setLoading(false);
});
}
useEffect(() => {
getItems();
}, []);
I don't get what I am doing wrong here. Any help would be much appreciated.
setTotalPrice(sum); won't reflect totalPrice immediately because setting state function is asynchronous. That's why console.log doesn't seem to be working properly.
Also, onSnapshot listener will be called every time collection data is changed (added/modified/removed). Here's an example for your information.
db.collection("cities").where("state", "==", "CA")
.onSnapshot(function(snapshot) {
snapshot.docChanges().forEach(function(change) {
if (change.type === "added") {
console.log("New city: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified city: ", change.doc.data());
}
if (change.type === "removed") {
console.log("Removed city: ", change.doc.data());
}
});
});
Related
So by default I have an state that shows loading, for example: [loading, setLoading] = useState(true), and I do have an useEffect called after the data comes from the api, and then iterate the data, and what I want to do is when the iteration and the stuff inside the map are completed call the setLoading(true) but this doesn't work.
Here is the source:
useEffect(() => {
if (orders.isLoading !== true && events.length === 0)
orders.orders.map((order) => {
const day = order.appointment.substring(0, 10);
const time = order.appointment.substring(11, 16);
const date = {
id: order._id,
day: new Date(day).getTime(),
title: ` Meeting with ${order.customer.firstName}`,
url: `/order/${order._id}`,
};
setEvents((prev) => [...prev, date]);
});
dispatchCalEvent({ type: 'fetch', payload: events });
setLoading(false);
}, [orders]);
The problem here is that the setLoading(false) is called before the iteration completes.
I think the below should work.
I've just added the i parameter to the map function to track the index and then inside the map loop, I check at the end if this index is the last one in the array, then call setIsLoading if it is.
useEffect(() => {
if (orders.isLoading !== true && events.length === 0)
orders.orders.map((order, i) => {
const day = order.appointment.substring(0, 10);
const time = order.appointment.substring(11, 16);
const date = {
id: order._id,
day: new Date(day).getTime(),
title: ` Meeting with ${order.customer.firstName}`,
url: `/order/${order._id}`,
};
setEvents((prev) => [...prev, date]);
if(i === orders.orders.length-1) setIsLoading(false);
});
dispatchCalEvent({ type: 'fetch', payload: events });
}, [orders]);
I have a function component and I am declaring a useState for a complex object like this:
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
I now try to set the state of the order by calling setOrder in a useEffect like this:
useEffect(() => {
(async function() {
let dh = new DataInterface("some string");
let errMsg = "";
// Get the sales order.
try
{
await dh.FetchOrder();
}
catch(error: any)
{
errMsg = error;
};
setOrder(salesOrder => ({...salesOrder, IsRetrieving: false, ErrorMsg: errMsg, DataInterface: dh}));
})();
}, []);
As is, this seems to work fine. However, I have a setInterval object that changes the screen message while order.IsRetrieving is true:
const [fetchCheckerCounter, setFetchCheckerCount] = useState<number>(0);
const statusFetcherWatcher = setInterval(() => {
if (order.IsRetrieving)
{
if (fetchCheckerCounter === 1)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "This can take a few seconds..."}));
}
else if (fetchCheckerCounter === 2)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "Almost there!.."}));
}
setFetchCheckerCount(fetchCheckerCounter + 1);
}
else
{
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
The issue is that order.IsRetrieving is always true for that code block, even though it does change to false, and my website changes to reflect that, even showing the data from dh.FetchOrder(). That means my timer goes on an infinite loop in the background.
So am I setting the state of order correctly? It's incredibly difficult to find a definite answer on the net, since all the answers are invariably about adding a new item to an array.
Issues
You are setting the interval as an unintentional side-effect in the function body.
You have closed over the initial order.isRetreiving state value in the interval callback.
Solution
Use a mounting useEffect to start the interval and use a React ref to cache the state value when it updates so the current value can be accessed in asynchronous callbacks.
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
const orderRef = useRef(order);
useEffect(() => {
orderRef.current = order;
}, [order]);
useEffect(() => {
const statusFetcherWatcher = setInterval(() => {
if (orderRef.current.IsRetrieving) {
if (fetchCheckerCounter === 1) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "This can take a few seconds...",
}));
} else if (fetchCheckerCounter === 2) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "Almost there!..",
}));
}
setFetchCheckerCount(counter => counter + 1);
} else {
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
return () => clearInterval(statusFetcherWatcher);
}, []);
I've been facing this problem.
My code now disappears when get a new message. I want to make messages pile up, but I have tried many ways, but I have not solved them. I would really appreciate it if you could tell me how to do this.
When I enter a message room, I get 25 messages from firebase server.
If there are more than 25 messages, last message disappears.
Disappear message. I don't want to like this ....
I want to like this.
here is my disappear code
const messageLimit = 25;
const [messagesSnapshot] = useCollection(
db
.collection('chats')
.doc(id)
?.collection('messages')
.orderBy('timestamp', 'desc')
.limit(messageLimit),
);
if (messagesSnapshot) {
const snap = messagesSnapshot.docs;
const startPost = messagesSnapshot.docs[messagesSnapshot.docs.length - 1];
setStartAt(startPost);
const messages = snap.map(message => ({
id: message.id,
user: message.data().user,
messageInfo: {
...message.data(),
timestamp: message.data().timestamp?.toDate().getTime(),
},
}));
setMessagesList(messages);
}
const getMoreMessages = async () => {
if (!lastPost) {
setIsGetMessagesLoading(true);
const query = await db
.collection('chats')
.doc(id)
.collection('messages')
.orderBy('timestamp', 'desc')
.startAfter(startAt)
.limit(messageLimit)
.get();
const messages = query.docs.map(message => ({
id: message.id,
user: message.data().user,
messageInfo: {
...message.data(),
timestamp: message.data().timestamp?.toDate().getTime(),
},
}));
setStartAt(query.docs[query.docs.length - 1]);
setMessagesList([...messagesList, ...messages]);
setIsGetMessagesLoading(false);
messages.length === 0 ? setLastPost(true) : setLastPost(false);
}
};
useEffect(() => {
showMessages();
}, [messagesSnapshot]);
As Shyam commented, you've limited the query to 25 messages, so when a new message is added, the oldest message falls out of the query.
One thing you could do is detect the changes between the updates, and never remove the "outdated" documents.
The example from the documentation:
db.collection("cities").where("state", "==", "CA")
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New city: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified city: ", change.doc.data());
}
if (change.type === "removed") {
console.log("Removed city: ", change.doc.data());
}
});
});
So you'd only handled added (which is initially every document) and possibly modified, but ignore removed.
Despite looking and following numerous answers here at stackoverflow,I have still failed to refactor this code to abide by the ESLint no-loop-func.
I keep getting the following warning, despite my efforts to refactor the code:
Compiled with warnings.
Function declared in a loop contains unsafe references to variable(s) 'lastResult', 'biologyBooks', 'page' no-loop-func
Here's the code:
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({ total: 0, biologyBooksByAuthor: [] });
let isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async() => { // fetch items
let page = 1;
let scienceBooks, biologyBooks;
// create empty arrays to store book objects for each loop
let scienceBooks = biologyBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do { // the looping - this is what I have failed to refactor
try {
await apiFullCall( // Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`
).then(res => {
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
body &&
body.results &&
body.results.map(eachBook => { // we map() over the returned "results" array
// the author with queried "author_id" writes science books;
// so we add each book (an object) into the science category
scienceBooks.push(eachBook);
// We then filter the author's biology books (from other science books)
biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof(is_biology) === "boolean" && is_biology === true
);
return null;
}
);
// increment the page with 1 on each loop
page++;
}
}
}).catch(error => console.error('Error while fetching data:', error));
} catch (err) { console.error(`Oops, something went wrong ${err}`); }
// keep running until there's no next page
} while (lastResult.next !== null);
// update the state
setState(prevState => ({
...prevState, total: scienceBooks.length, biologyBooksByAuthor: biologyBooks,
}));
};
React.useEffect(() => { // fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
};
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
}
Please note that I actually declared the said variables lastResult, biologyBooks and page outside the "do-while".
Any help or clues will be greatly appreciated.
The function the warning is referring to is the .then callback, if you're using async/await stick to it, try removing the .then part by assigning the result to a variable instead and remove the unnecessary .map, you can concatenate previous results with spread operator or .concat.
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({
total: 0,
scienceBooksByAuthor: [],
});
const isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async () => {
// fetch items
let page = 1;
let scienceBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do {
// the looping - this is what I have failed to refactor
try {
const res = await apiFullCall(
// Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`,
);
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
// concatenate new results
scienceBooks = [
...scienceBooks,
...((lastResult && lastResult.results) || []),
];
// increment the page with 1 on each loop
page += 1;
}
}
} catch (err) {
console.error(`Oops, something went wrong ${err}`);
}
// keep running until there's no next page
} while (lastResult.next !== null);
const biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof is_biology === 'boolean' && is_biology === true,
);
// update the state
setState(prevState => ({
...prevState,
total: scienceBooks.length,
scienceBooksByAuthor: scienceBooks,
}));
};
React.useEffect(() => {
// fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
}
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
};
When page loaded first time, I need to get all information, that is why I am calling a fetch request and set results into State [singleCall function doing that work]
Along with that, I am connecting websocket using socket.io and listening to two events (odds_insert_one_two, odds_update_one_two), When I got notify event, I have to
check with previous state and modify some content and update the state without calling again fetch request. But that previous state is still [] (Initial).
How to get that updated state?
Snipptes
const [leagues, setLeagues] = useState([]);
const singleCall = (page = 1, params=null) => {
let path = `${apiPath.getLeaguesMatches}`;
Helper.getData({path, page, params, session}).then(response => {
if(response) {
setLeagues(response.results);
} else {
toast("Something went wrong, please try again");
}
}).catch(err => {
console.log(err);
})
};
const updateData = (record) => {
for(const data of record) {
var {matchId, pivotType, rateOver, rateUnder, rateEqual} = data;
const old_leagues = [...leagues]; // [] becuase of initial state value, that is not updated
const obj_index = old_leagues.findIndex(x => x.match_id == matchId);
if(obj_index > -1) {
old_leagues[obj_index] = { ...old_leagues[obj_index], pivotType, rateOver: rateOver, rateUnder:rateUnder, rateEqual:rateEqual};
setLeagues(old_leagues);
}
}
}
useEffect(() => {
singleCall();
var socket = io.connect('http://localhost:3001', {transports: ['websocket']});
socket.on('connect', () => {
console.log('socket connected:', socket.connected);
});
socket.on('odds_insert_one_two', function (record) {
updateData(record);
});
socket.on('odds_update_one_two', function (record) {
updateData(record);
});
socket.emit('get_odds_one_two');
socket.on('disconnect', function () {
console.log('socket disconnected, reconnecting...');
socket.emit('get_odds_one_two');
});
return () => {
console.log('websocket unmounting!!!!!');
socket.off();
socket.disconnect();
};
}, []);
The useEffect hook is created with an empty dependency array so that it only gets called once, at the initialization stage. Therefore, if league state is updated, its value will never be visible in the updateData() func.
What you can do is assign the league value to a ref, and create a new hook, which will be updated each time.
const leaguesRef = React.useRef(leagues);
React.useEffect(() => {
leaguesRef.current = leagues;
});
Update leagues to leaguesRef.current instead.
const updateData = (record) => {
for(const data of record) {
var {matchId, pivotType, rateOver, rateUnder, rateEqual} = data;
const old_leagues = [...leaguesRef.current]; // [] becuase of initial state value, that is not updated
const obj_index = old_leagues.findIndex(x => x.match_id == matchId);
if(obj_index > -1) {
old_leagues[obj_index] = { ...old_leagues[obj_index], pivotType, rateOver:
rateOver, rateUnder:rateUnder, rateEqual:rateEqual};
setLeagues(old_leagues);
}
}
}