Firestore onSnapshot() and useEffect - reactjs

so I am currently working on a project, where I need a realtime datastream of a pool object and a user object. As I am using Firebase as a BAAS, I tried to refactor my code with onSnapshot, as this seemed to minimize requests to the database.
Objective: On Mounting of the component, set a realtime Listener on the pool object, which changes the state which stores the poolData, everyTime there is a change. Based on this up to date pool data, run a calculation every Second, which also updates other states.
Status Quo: When I am rebuilding my project (nextjs) it works for a few instances but then an error occurs when trying to access the pool object Error occured in getting Pool Time TypeError: Cannot read properties of null
I was thinking that maybe it throws an error because the standard value for state is set as null, so in the useEffect hook I tried to conventionally fetch a snapshot of the data first, but this also didn't seem to work. Furthermore I am pretty sure I am messing something up regarding the intricacies of useEffect and onSnapshot.
I really appreciate your help and your input!
const router = useRouter();
const slug = router.query.slug;
const profile = `/profile/${auth.currentUser.uid}`
const [timerDisplay, setTimerDisplay] = useState(0);
const [finalTime, setFinalTime] = useState(0);
const [tokenState, setTokenState] = useState(0);
const [open, setOpen] = useState(true);
const [currentlyWinning, setCurrentlyWinning] = useState("Nobody");
const [poolData, setPoolData] = useState(undefined);
console.log(poolData);
useEffect(() => {
getFirstPoolSnap();
const unsubPoolListener = onSnapshot(doc(db, "pools", slug), (doc) => {
console.log(doc.data());
setPoolData(doc.data());
});
const unsubTokenListener = onSnapshot(
doc(db, "users", auth.currentUser.uid),
(doc) => {
setTokenState(doc.data().tokens);
}
);
const interval = setInterval(() => {
getPoolTime();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
function getPoolTime() {
try {
let localTime = Math.floor(Date.now() / 1000);
const gbTimeLeft =
poolData.expTimestamp.toMillis() / 1000 - localTime;
const finalTimeLeft =
poolData.finalTime.toMillis() / 1000 - localTime;
setOpen(poolData.open);
if (gbTimeLeft < 0 || finalTimeLeft < 0) {
setTimerDisplay(0);
} else {
setTimerDisplay(Math.floor(gbTimeLeft));
}
if (finalTimeLeft < 0 || gbTimeLeft < 0) {
setFinalTime(0);
} else {
setFinalTime(Math.floor(finalTimeLeft));
}
setCurrentlyWinning(poolData.lastSetterName);
} catch (error) {
console.log("Error occured in getting Pool Time", error);
}
}

Related

Get the selected seats array into the state variable React Hooks

I'm getting the selected checkbox name into an array. Then, I want to use it later for some calculations. but when I try to get the values it's giving the wrong values. I want to allow users to select the ticket count by using the +/- buttons. following is my code
Code
Getting the selected seat from the checkboxes
const [seatCount, setSeatCount] =React.useState("");
const [open, setOpen] = React.useState(false);
const [counter, setCounter] = React.useState(0);
var seatsSelected = []
const handleChecked = async (e) => {
let isChecked = e.target.checked;
if(isChecked){
await seatsSelected.push(e.target.name)
} else {
seatsSelected = seatsSelected.filter((name) => e.target.name !== name);
}
console.log(seatsSelected);
}
calculations happening on a dialog
const handleClickOpen = () => { //open the dialog
setOpen(true);
console.log(seatsSelected);
const seatTotal = seatsSelected.length
console.log(seatTotal)
setSeatCount(seatTotal)
console.log(seatCount)
};
const handleClose = () => { //close the dialog
setOpen(false);
};
const handleIncrement = () =>{ //increse the count
if(counter < seatsSelected.length){
setCounter(counter + 1)
} else {
setCounter(counter)
}
}
const handleDecrement = () =>{ //decrese the count
if(counter >= seatsSelected.length){
setCounter(counter - 1)
} else {
setCounter(counter)
}
}
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(seatCount);
}, [seatCount);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.

How to fetch data from all documents in a subcollection in firebase firestore using React?

For my project, I want to fetch data from all documents in a subcollection. And there are multiple documents with this subcollection.
To clarify, this is how my firestore is strctured:
I have an events collection which contains multiple documents with doc.id being the event name itself. Each event document has several fields and an attendee subcollection. In the attendee subcollection, each document contains details about the attendee.
I want to map through all documents in the events collection and fetch data about attendees from all of them.
And I want to display this data when the component first renders. So I'm calling the function inside useEffect. Here's what I have tried:
const [attendeeInfo, setAttendeeInfo] = useState({});
const [events, setEvents] = useState([]);
const getEventsData = async () => {
// first of all, we need the name of the user's org
// fetch it from users collection by using the uid from auth
const orgRef = doc(db, "users", auth["user"].uid);
const orgSnap = await getDoc(orgRef);
// now that we have the org name, use that to search events...
// ...created by that org in events collection
const eventsRef = collection(db, "events");
const eventsQuery = query(eventsRef, where("createdBy", "==", orgSnap.data().orgName));
const eventsQuerySnapshot = await getDocs(eventsQuery);
let eventsInfo = [];
eventsQuerySnapshot.forEach((doc) => {
eventsInfo.push(doc.id);
})
setOrg(orgSnap.data().orgName);
setEvents(eventsInfo);
}
const getAttendeesData = (events) => {
console.log(events);
let attendeeInformation = [];
events.forEach(async (event) => {
const attendeesRef = collection(db, "events", event, "attendees");
const attendeesSnap = await getDocs(attendeesRef);
attendeesSnap.forEach((doc) => {
const isItMentor = doc.data().isMentor ? "Yes" : "No";
const isItMentee = doc.data().isMentee ? "Yes" : "No";
const attendeeData = {
name: doc.id,
mentor: isItMentor,
mentee: isItMentee,
};
attendeeInformation.push(attendeeData);
})
})
// console.log(attendeeInformation);
setAttendeeInfo(attendeeInformation);
}
useEffect(() => {
getEventsData();
// console.log(attendeeInfo);
getAttendeesData(events);
}, []);
However, when I console log the events inside my attendeesData function, I get an empty array which means that the events state variable hasn't been updated from previous function.
Can anyone help me solve this?
This is a timing issue. On first render you start fetching the list of events, but you aren't waiting for them to be retrieved before using them. Furthermore, because you only run this code on mount, when events is eventually updated, getAttendeesData won't be invoked with the updated array.
useEffect(() => {
getEventsData(); // <-- queues and starts "fetch event IDs" action
getAttendeesData(events); // <-- starts fetching attendees for `events`, which will still be an empty array
}, []); // <-- [] means run this code once, only when mounted
The solution to this is to split up the useEffect so each part is handled properly.
useEffect(() => {
getEventsData(); // <-- queues and starts "fetch event IDs" action
}, []); // <-- [] means run this code once, only when mounted
useEffect(() => {
getAttendeesData(events); // initially fetches attendees for an empty array, but is then called again when `events` is updated with data
}, [events]); // <-- [events] means run this code, when mounted or when `events` changes
Next, you need to fix up getAttendeesData as it has a similar issue where it will end up calling setAttendeeInfo() at the end of it with another empty array (attendeeInformation) because you aren't waiting for it to be filled with data first. While this array will eventually fill with data correctly, when it does, it won't trigger a rerender to actually show that data.
const [attendeeInfo, setAttendeeInfo] = useState([]); // <-- should be an array not an object?
const [events, setEvents] = useState([]);
const getAttendeesData = async (events) => {
console.log(events);
const fetchAttendeesPromises = events.map(async (event) => {
const attendeesRef = collection(db, "events", event, "attendees");
const attendeesSnap = await getDocs(attendeesRef);
const attendeeInformation = [];
attendeesSnap.forEach((doc) => {
const isItMentor = doc.data().isMentor ? "Yes" : "No";
const isItMentee = doc.data().isMentee ? "Yes" : "No";
const attendeeData = {
name: doc.id,
mentor: isItMentor,
mentee: isItMentee,
};
attendeeInformation.push(attendeeData);
})
return attendeeInformation; // also consider { event, attendeeInformation }
})
// wait for all attendees to be fetched first!
const attendeesForAllEvents = await Promises.all(fetchAttendeesPromises)
.then(attendeeGroups => attendeeGroups.flat()); // and flatten to one array
// console.log(attendeesForAllEvents);
setAttendeeInfo(attendeesForAllEvents);
}
Applying these changes in a basic and incomplete (see below) way, gives:
// place these outside your component, they don't need to be recreated on each render
const getEventsData = async () => { /* ... */ }
const getAttendeesData = async (events) => { /* ... */ }
export const YourComponent = (props) => {
const [attendeeInfo, setAttendeeInfo] = useState(null); // use null to signal "not yet loaded"
const [events, setEvents] = useState(null); // use null to signal "not yet loaded"
const loading = events === null || attendeeInfo === null;
useEffect(() => {
getEventsData();
}, []);
useEffect(() => {
if (events !== null) // only call when data available
getAttendeesData(events);
}, [events]);
// hide component until ready
// consider rendering a spinner/throbber here while loading
if (loading)
return null;
return (
/* render content here */
)
}
Because getEventsData() and getAttendeesData() are Promises, you should make use of useAsyncEffect implmentations like #react-hook/async and use-async-effect so you can handle any intermediate states like loading, improper authentication, unmounting before finishing, and other errors (which are all not covered in the above snippet). This thread contains more details on this topic.

Undefined is not an object (evaluating '_ref5.x') caused by order of setState functions?

const [onePrices, setOnePrices] = useState([]);
const [twoPrices, setTwoPrices] = useState([]);
const [threePrices, setThreePrices] = useState([]);
const [fourPrices, setFourPrices] = useState([]);
const [fivePrices, setFivePrices] = useState([]);
const [terminal, setTerminal] = useState('0');
const dayToArray = {
1: [onePrices, setOnePrices],
2: [twoPrices, setTwoPrices],
3: [threePrices, setThreePrices],
4: [fourPrices, setFourPrices],
5: [fivePrices, setFivePrices],
};
useEffect(() => {
const setData = async () => {
//dayToArray is a map which maps from a day to the corresponding
//[state, setState] for that day
const arr = dayToArray[day][0];
const setArr = dayToArray[day][1];
//If I already saved the values I don't want to query the API again
if (arr.length > 0) {
setTerminal(arrRef[arr.length - 1].val);
return;
}
//Haven't gotten this data yet => query the API and store the data
const data = await APIService(day, key);
setArr(data);
setTerminal(data[data.length - 1].val);
};
setData();
}, [day]);
In this order it generates the error, however, when I switch up the order of the state changes the error disappears.
setTerminal(data[data.length - 1].val);
setArr(data);
I don't understand React Native that well and was wondering why I'm receiving this error and why swapping the order solves it. Also if I'm doing something stupid I would appreciate any feedback.

Update non state variable in React useEffect

I have an application the receives new data over a WebSocket every second. Each second I receive 10 to 15 messages that I need to store in and display. I am currently updating a state array each time I receive new data but the effect is that I re-render the screen 10 to 15 times per second.
What I want to achieve is to store the incoming data in an array but only update the screen once every second.
My approach that I can't get working is to create a non-state array that is updated when new data is received and copy that data to a state array every second with a timer.
This is the declaration of the state array:
const [boatData2, _setBoatData2] = useState({});
const boatDataRef = useRef(boatData2);
const setBoatData2 = (update) => {
boatDataRef.current = update;
_setBoatData2(update);
}
This is the hook code where the data is received:
useEffect(() => {
if (!ws.current) return;
ws.current.onmessage = e => {
setDataFlowing(true);
setDataAge(0);
setScreenUpdates(screenUpdates => screenUpdates + 1);
//console.log('New Data');
const message = JSON.parse(e.data);
if (message.updates && message.updates.values) {
message.updates[0].values.forEach(obj => {
let newPath = obj.path.split('.').join("");
const update = {
path: obj.path,
value: obj.value,
timestamp: message.updates[0].timestamp,
valid: true,
age: 0,
};
now = Date.parse(message.updates[0].timestamp);
setBoatData2({ ...boatDataRef.current, [newPath]: update });
});
}
};
}, []);
This is the code that runs every second:
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
let boatdata = boatData2;
//console.log(boatData3);
Object.values(boatdata).forEach(val => {
val.age = val.age + 1;
if (val.age > 30) {
val.valid = false;
}
});
setBoatData2(boatdata);
setDataAge(dataAge => dataAge + 1);
if (dataAge > 60) {
setDataFlowing(false);
}
}, 1000);
} else if (!isActive && seconds !== 0) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isActive, seconds, boatData2]);
You can do this with the help of useRef .
const messageRef = useRef([]);
This creates a object inside messageRef called current which we can mutate and mutating it will not trigger a re-render. Now your messageRef will be something like this
{
current: []
}
Now whenever you get the message from the websocket push the message into this ref as
messageRef.current.push(your message)
Now inside your function which updates the state after some xyz second . You can use this Ref to update the state
setYourMessages(messageRef.current);
messageRef.current = []; // do this after you state update call. Else you will be pushing duplicate messages into the state

React/reselect how to refresh ui before a selector is recomputed

In my React-App (create-react-app) I use a selector (created with reselect) to compute derived data from stored data.The Problem is, the selector takes a long time to compute. I would like to show a spinner (or a message) on the user interface. But each time the selector is recomputed the ui freezes.
I read a lot of stuff (Web Worker, requestIdleCallback, requestAnimationFrame) and try to make my own React hook but I am stuck. I cannot use the selector in callbacks.
The searched result is simply to get the ui refreshed before the selector is recomputed.
That's my solution. I don't know if it's "good" or if it breaks some rules of react or reselect, but it works. Maybe you can assess that?The code is simplified to improve readability.
The idea is, the selector returns a Promise and I call requestAnimationFrame before computing the data to get a chance to refresh the ui.
selector.js:
const dataSelector = state => state.data;
export const getDataComputedPromise = createSelector([dataSelector], (data) => {
const compute = function () {
return new Promise((resolve) => {
// heavy computing stuff
resolve(computedData);
});
};
return new Promise((resolve) => {
let start = null;
let requestId = null;
function step (timestamp) {
if (!start) {
start = timestamp;
window.cancelAnimationFrame(requestId);
requestId = window.requestAnimationFrame(step);
return;
};
compute().then(freeResources => {
window.cancelAnimationFrame(requestId);
resolve(freeResources);
});
}
requestId = window.requestAnimationFrame(step);
});
});
myPage.js
const MyPage = ({ someProps }) => {
...
const dataComputedPromise = useSelector(getDataComputedPromise);
const [dataComputed, setDataComputed] = useState([]);
const [computeSelector, setComputeSelector] = useState(false);
useEffect(() => {
setComputeSelector(true);
}, [data]);
useEffect(() => {
dataComputedPromise.then(dataComputed => {
setDataComputed(dataComputed);
setComputeSelector(false);
});
}, [dataComputedPromise]);
...
return <div>
<Loader active={compueSelector}>Computing data...</Loader>
</div>;
};
export default MyPage;

Resources