I am synchronising clocks in two JavaScript clients by writing to Firestore every second and then subscribing the second remote client / "slave" to the document.
This works fine and I can read the document changes in real time based on this method of creating the document reference:
const useCloudTimer = (isMaster, opponentCCID, userCCID) => {
const dispatch = useDispatch();
const { localRemainingTimeMS, clockTimeOut } = useSelector((state) => state.clock);
const timerRef = useRef(null);
useEffect(() => {
const db = firebase.firestore();
timerRef.current = db.collection('timers').doc(`${isMaster
? userCCID + opponentCCID
: opponentCCID + userCCID
}`);
dispatch(clock(CLOCK, { cloudTimerDbRef: timerRef.current }));
}, []);
const pushTimer = async (duration) => {
try {
await timerRef.current.set({ duration });
} catch (error) {
logger.error(error, 'cloud timer');
}
};
useEffect(() => { if (isMaster) pushTimer(localRemainingTimeMS); }, [localRemainingTimeMS]);
const getTimer = async () => {
try {
const unsubscribeRemoteTimer = await timerRef.current.onSnapshot((doc) => {
if (!clockTimeOut && doc.exists) {
const duration = Number(doc.data().duration);
dispatch(clock(CLOCK, { remoteRemainingTimeMS: duration }));
}
});
if (clockTimeOut) unsubscribeRemoteTimer().then((arg) => console.log('unsubscribeRemoteTimer', arg));
} catch (error) {
logger.error(error, 'getTimer');
}
};
useEffect(() => { if (!isMaster) getTimer(); }, []);
};
export default useCloudTimer;
The problem is when I want to delete the document. If the client that did not create the document tries to delete it, what happens is that a new document is created for a split second with the same name, and then that one is deleted. Here is the exact moment this happens where you can see two documents with the same name:
A document is green when it's being written to and red when it's being deleted.
My document ref is being stored in redux and then used via the store when required:
export const deleteCloudTimer = async (timerRef) => {
if (timerRef) {
try {
await timerRef.delete();
} catch (error) {
logger.error(error, 'Error removing document: ');
}
}
};
How can my Firebase client app delete a document if it didn't create it?
Tasks requiring precision timing especially with high amounts of reads/writes are not suggested for firestore. You might consider an alternative GCP product instead?
Related
I am hosting a react app in aws amplify using the aws-serverless version of express as the REST API, which sits inside of a lambda function. A big problem that I am facing is that asynchronous jobs in aws-serverless express cause the lambda function to complete before the promises resolve. Leaving me with no data and no error handling. This caused me to bring a lot of the asynchronous work to the front end of the application.
The problem here is that I need to bring a large amount of data into state. Right now, I am using a delay workaround (shown below) but instead need a programatic way to make sure state is finished updating before being used in the second useEffect hook (dependent on odds & failedTries props) instead of using the delay functionality.
Any help would be greatly appreciated.
const App = ({ signOut }) => {
const [odds, setOdds] = useState([]);
const [updateTime,setUpdateTime] = useState(0);
const [failedTries,setFailedTries] = useState(0);
useEffect(() => {
const setNflOdds = async () => {
let response = await updateNflOdds();
let data = response;
setOdds(data);
};
setNflOdds();
setUpdateTime(1);
const interval = setInterval(() => {
setNflOdds();
setUpdateTime(updateTime => updateTime +1);
}, 100000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const s3Push = (() => {
if(!odds.length) {
setFailedTries(failedTries => failedTries + 1);
} else {
const delay = ms => new Promise(res => setTimeout(res, ms));
const nflOddsRefDelay = async() => {
*//This is the current workaround, wait ten seconds before pushing odds state up to the s3 bucket*
await delay(10000);
oddsS3Helper(odds);
};
nflOddsRefDelay()
}
});
s3Push();
}, [odds, failedTries]);
With the above indicated delay workaround this works for my use case (13k records inside of the array) but the data size is highly variable and I want to figure out a way that no matter the data size it brings the entire call up to the s3 bucket.
below is the content of the functions being called in the useEffect hook
const pushToS3 = async ( file, key ) => {
const creds = await Auth.currentCredentials()
const REGION = {region};
const s3Client = new S3Client({
credentials: Auth.essentialCredentials(creds),
region: REGION
});
const params = {
Bucket: {s3 bucket name}
Key: key,
Body: file,
};
s3Client.send(new PutObjectCommand(params));
console.log("file is sent");
};
const oddsS3Helper = (async (odds) => {
console.log("inside s3 helper: ",odds);
let csv = '';
let headers = Object.keys(odds[0]).join(',');
let values = odds.map(odd => Object.values(odd).join(',')).join('\n');
csv += headers + '\n' + values;
const buffedFile = csv;
const key = 'nflprops.csv'
const delay = ms => new Promise(res => setTimeout(res, ms));
const propRefDelay = async() => {
await delay(5000);
await postNflOdds();
};
pushToS3( buffedFile, key );
await propRefDelay();
});
async function getNflGames() {
const apiName = {name of serverless API inside of lambda};
const path = {path name};
const init = {
headers: {} // OPTIONAL
};
const data = await API.get(apiName, path, init);
return data;
};
async function getNflOdds(gameId) {
const apiName = {name of serverless API inside of lambda};
const path = {path name};
const init = {
headers: {}, // OPTIONAL
body: { gameId }
};
const data = await API.post(apiName, path, init);
return data;
};
async function updateNflOdds() {
const ojNflGames = await getNflGames();
const nflGameProps = [];
const nflOddsPush = ( async () => {
try {
await ojNflGames.data.map( async (game) => {
const ojNflOdds = await getNflOdds(game.id)
await ojNflOdds.data[0].odds.map((line) => {
nflGameProps.push(
{
gameId: game.id,
oddsId: line.id,
sports_book_name: line.sports_book_name,
name: line.name,
price: line.price,
checked_date: line.checked_date,
bet_points: line.bet_points,
is_main: line.is_main,
is_live: line.is_live,
market_name: line.market_name,
home_rotation_number: line.home_rotation_number,
away_rotation_number: line.away_rotation_number,
deep_link_url: line.deep_link_url,
player_id: line.player_id,
}
);
});
});
} catch (err) {
console.log("there was an error", err);
}
});
try {
await nflOddsPush();
} catch(err) {
console.log("odds push errored: ", err);
}
console.log("inside of updateNflOdds function: ",nflGameProps);
return nflGameProps;
};
I’m starting a new application that will serve as the basis for a real app
It works fine, but Firestore data won't load until I change the code in React
// I have one collection with subcollections,
// first collection is oses (operating systems) and the others are:
// The real Operating systems, that are children of the main ones
// In this case, Windows and macOS
export default function App() {
const [categories, setCategories] = useState({});
const [startProcessDone, setstartProcessDone] = useState(false);
const [firstPhaseDone, setFirstPhaseDone] = useState(false);
const [secondPhaseDone, setSecondPhaseDone] = useState(false);
useEffect(() => {
setCategories({});
setstartProcessDone(true);
}, []);
useEffect(() => {
console.log("in useEffect 1");
//Here, I read the main ones, which are, in this case
// Windows and macOS
const readDocs = async (collectionRef) => {
const result = {};
try {
const querySnapshot = await getDocsFromServer(collectionRef);
querySnapshot.docs.forEach((doc) => {
result[doc.id] = { ...doc.data() };
});
setCategories(result);
} catch (error) {
console.log("error::: ", error);
}
};
const collectionRef = collection(db, "oses");
readDocs(collectionRef);
setFirstPhaseDone(true);
}, [startProcessDone]);
useEffect(() => {
console.log("in useEffect 2");
const readDocs = async (prop, collectionRef) => {
let newOperatingSystems = [];
let newOperatingSystemObj = {};
try {
const querySnapshot = await getDocsFromServer(collectionRef);
querySnapshot.docs.forEach((doc) => {
newOperatingSystemObj = { ...doc.data() };
newOperatingSystems[newOperatingSystemObj.name] =
newOperatingSystemObj;
});
let newCategories = { ...categories };
newCategories[prop].items = newOperatingSystems;
setCategories(newCategories);
} catch (error) {
console.log("error:: ", error);
}
};
for (var prop in categories) {
// And for each of the main ones, we have their children, which are, in this case
// for Windows: Windows XP and Windows Vista
// for macOS: Big Sur
//These subcollections are children of the main ones
const collectionRef = collection(db, `oses/${prop}/children`);
readDocs(prop, collectionRef);
}
setSecondPhaseDone(true);
}, [firstPhaseDone]);
useEffect(() => {
console.log("in useEffect 3.");
// The result is:
console.log("categories:: ", categories);
}, [secondPhaseDone]);
return (
<div>
<h1>Operating Systems</h1>
</div>
);
}
The result is
When I change some code, this is what it shows:
The data is ok
But if I just restart it using
It won't load any data:
What could I be missing here
Thanks in advance
Rafael
After reviewing it a little deeper, I think that it has to do with some kind of cache of FireStore
I tried with another simpler code, and things were the same for the console.logs but the software works fine, even though the console.logs were wrong
I don't understand, but it works
Rafael
Can you please try doing this way ?
useEffect(() => { fetchDocs(); }, [])
const fetchDocs = async() => { const response =
db.collection('name'); }
Using asynch inside useEffect is never a good idea. It might cause memory leak.
I have this react-app which displays a table with a lot of data.
And i need to know when a row of the table gets removed from firebase, but i need to do that realtime and save that data in a reducer dispatching an action so i can access that data in the rest of the app.
And I did it with an useEffect
useEffect(() => {
const unsubscribe = onSnapshot(
config(terminal!),
(snapshot) => {
// let addedData: any[] = [];
let deletedData: any[] = [];
snapshot.docChanges().forEach((change) => {
if (change.type === 'removed') {
deletedData = [...deletedData, change.doc.data()];
}
});
if (deletedData.length > 0) {
dispatch(realtimePickups(deletedData, 'removed'));
}
},
(error) => {
console.log(error);
},
);
return () => unsubscribe();
}, []);
So, everytime something gets removed, i would dispatch an action, my problem is, is this a good way of doing this ?¿ Would i have performance problems ?¿ Would there be a problem if i dispatch like 50 actions if for some reason 50 rows get removed ?¿
If this is not the best way to do it, would you help me to improve my code.
This line:
dispatch(realtimePickups(deletedData, 'removed'));
Can cause too many renders depending on the volume of firebase subscription calls. This also depends on two more factors:
Is the deleted data used in a component and is mounted in DOM?
Are you using an immutable data store?
I recommend a different variation:
Install use-debounce. It has a useDebouncedCallback hook which we'll need to use.
Then:
import { useDebouncedCallback } from 'use-debounce';
let deletedData: any[] = [];
const MyComponent = () => {
// Debounce massive updates by 1second
const deletedDataDispatch = useDebouncedCallback((data) => {
dispatch(data)
deletedData = []
}, 1000);
useEffect(() => {
const unsubscribe = onSnapshot(
config(terminal!),
(snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'removed') {
deletedData = [...deletedData, change.doc.data()];
}
});
if (deletedData.length > 0) {
deletedDataDispatch(realtimePickups(deletedData, 'removed'));
}
},
(error) => {
console.log(error);
})
return () => unsubscribe();
}, [])
}
I am trying to refactor my code to work solo with firebase the issue that I am facing is firebase methods for example onSnapshot, could this method totally replace the need for the classic first data fetch ? I am ruining in to bunch of run time errors since many data are not available at start and I am starting to doubt my onSnapShot, below is the code to my entry component where most of the data fetching using onSnapShot happens but still many values at start are undefined any idea how to tackle such issue ? the data are there but at start they are not,
useEffect(() => {
context.SetUser('trainer');
const fetchClient = () => {
const colReClients = collection(db, 'Clients');
const id: string = _authContext?.currentUser?.uid;
const unsub = onSnapshot(doc(colReClients, id), (doc) => {
console.log(doc.data(), 'snapShot triggered');
///////////// Refactor to foreach
context.SetExerciseStateBackEnd('dataCenter', doc.data()?.clients);
//context.SetExerciseStateBackEnd('focus', doc.data()?.focus);
});
return () => unsub();
};
if (_authContext?.currentUser) {
fetchClient();
}
}, [_authContext?.currentUser?.uid]);
useEffect(() => {
const fetchWeeks = async () => {
// ADD WEEKS
// Weeks Collection Reference
const colRefWeeks = collection(db, 'Weeks');
const docRefWeeks = doc(colRefWeeks, `${context.focus}`);
const unsub = onSnapshot(docRefWeeks, (doc) => {
console.log(doc.data(), 'snapShot triggered');
context.SetExerciseStateBackEnd('weeks', { [`${context.focus}`]: doc.data()?.clientWeeks });
const arr = doc.data()?.clientWeeks.find((el: any) => el.weekId === context.weekFocus).routines;
if (arr) {
context.SetExerciseStateBackEnd('routines', arr);
}
});
return () => unsub();
};
fetchWeeks();
}, [context.focus]);
I'm trying to make a Tweets Application with React and Firebase and I have been suffering when trying to get info from more than 1 collection.
So this is the story:
I get the tweets using onSnapshot. All fine here
I need more info from 2 other collections: user_preferences and user_photo, so I use .get() within the onSnapshot
For managing asynchronism, I resolve my 2 promises before returning the tweet data + details data object for my map function.
I made a console.log of my mappedTweet and the values are OKEY. Here I can see the tweet data + details data
But my STATE "tweets" just have an array of undefined objects =(. It shows the right number of rows accoroding to my Tweets collection but rows of undefined data, and not the rows of my mappedTweets objects. Why?
Can anyone shed some light?
useEffect(() => {
//------------getting the TWEETS with onSnapshot()-------------
const cancelSuscription = firestore
.collection('tweets')
.onSnapshot((snapshot) => {
const promises = [];
const tweetsMapped = snapshot.docs.map((doc) => {
let tweetAndAuthor;
const tweetMappped = {
text: doc.data().text,
likes: doc.data().likes,
email: doc.data().email,
created: doc.data().created,
uid: doc.data().uid,
id: doc.id,
};
let authorPreference, authorPhoto;
const userPreferencePromise = firestore
.collection('user_preferences')
.where('uid', '==', tweetMappped.uid)
.get();
const userPhotoPromise = firestore
.collection('user_photos')
.where('id', '==', tweetMappped.uid)
.get();
promises.push(userPreferencePromise);
promises.push(userPhotoPromise);
//------------getting the AUTHOR USER PREFERENCES with .get()-------------
userPreferencePromise.then((snapshot2) => {
authorPreference = snapshot2.docs.map((doc) => {
return {
username: doc.data().username,
color: doc.data().color,
};
});
});
//------------getting the AUTHOR PHOTO with .get()-------------
userPhotoPromise.then((snapshot3) => {
authorPhoto = snapshot3.docs.map((doc) => {
return {
photoURL: doc.data().photoURL,
};
});
});
Promise.all(promises).then((x) => {
return {
...tweetMappped,
author: authorPreference[0].username,
authorColor: authorPreference[0].color,
authorPhoto: authorPhoto[0].photoURL,
};
});
});
Promise.all(promises).then((x) => {
setTweets(tweetsMapped);
});
});
return () => cancelSuscription();
}, []);
Well, I made it work by changing the model I was using to retrieve the data from Firebase.
I was using an outer onSnapshot with nested promises (I think I was very near here), but now I'm using nested onSnapshots and now app is behaving as expected.
So this is the new useEffect
useEffect(() => {
let cancelUserPrefSuscription, cancelUserPhotoSuscription;
// First onSnapshot
const cancelTweetSuscription = firestore
.collection('tweets')
.onSnapshot((tweetSnapshot) => {
const list = [];
tweetSnapshot.docs.forEach((tweetDoc) => {
//Second onSnapshot
cancelUserPrefSuscription = firestore
.collection('user_preferences')
.where('uid', '==', tweetDoc.data().uid)
.onSnapshot((userPrefSnapshot) => {
userPrefSnapshot.docs.forEach((userPrefDoc) => {
//Third onSnapshot
cancelUserPhotoSuscription = firestore
.collection('user_photos')
.where('id', '==', tweetDoc.data().uid)
.onSnapshot((userPhotoSnapshot) => {
userPhotoSnapshot.docs.forEach((userPhotoDoc) => {
//Taking the whole data i need from all snapshots
const newData = {
id: tweetDoc.id,
...tweetDoc.data(),
author: userPrefDoc.data().username,
authorColor: userPrefDoc.data().color,
authorPhoto: userPhotoDoc.data().photoURL,
};
list.push(newData);
//Updating my state
if (tweetSnapshot.docs.length === list.length) {
setTweets(list);
}
});
});
});
});
});
});
return () => {
cancelTweetSuscription();
cancelUserPrefSuscription();
cancelUserPhotoSuscription();
};
}, []);
Edit: Fix from comments of above code
Author: #samthecodingman
For each call to onSnapshot, you should keep track of its unsubscribe function and keep an array filled with the unsubscribe functions of any nested listeners. When an update is received, unsubscribe each nested listener, clear the array of nested unsubscribe functions and then insert each new nested listener into the array. For each onSnapshot listener attached, a single unsubscribe function should be created that cleans up the listener itself along with any nested listeners.
Note: Instead of using this approach, create a Tweet component that pulls the author's name and photo inside it.
useEffect(() => {
// helper function
const callIt = (unsub) => unsub();
// First onSnapshot
const tweetsNestedCancelListenerCallbacks = [];
const tweetsCancelListenerCallback = firestore
.collection('tweets')
.onSnapshot((tweetSnapshot) => {
const newTweets = [];
const expectedTweetCount = tweetSnapshot.docs.length;
// cancel nested subscriptions
tweetsNestedCancelListenerCallbacks.forEach(callIt);
// clear the array, but don't lose the reference
tweetsNestedCancelListenerCallbacks.length = 0;
tweetsNestedCancelListenerCallbacks.push(
...tweetSnapshot.docs
.map((tweetDoc) => { // (tweetDoc) => Unsubscribe
const tweetId = tweetDoc.id;
//Second onSnapshot
const userPrefNestedCancelListenerCallbacks = [];
const userPrefCancelListenerCallback = firestore
.collection('user_preferences')
.where('uid', '==', tweetDoc.data().uid)
.limitToFirst(1)
.onSnapshot((userPrefSnapshot) => {
const userPrefDoc = userPrefSnapshot.docs[0];
// cancel nested subscriptions
userPrefNestedCancelListenerCallbacks.forEach(callIt);
// clear the array, but don't lose the reference
userPrefNestedCancelListenerCallbacks.length = 0;
//Third onSnapshot
const userPhotoCancelListenerCallback = firestore
.collection('user_photos')
.where('id', '==', tweetDoc.data().uid)
.limitToFirst(1)
.onSnapshot((userPhotoSnapshot) => {
const userPhotoDoc = userPhotoSnapshot.docs[0];
// Taking the whole data I need from all snapshots
const newData = {
id: tweetId,
...tweetDoc.data(),
author: userPrefDoc.data().username,
authorColor: userPrefDoc.data().color,
authorPhoto: userPhotoDoc.data().photoURL,
};
const existingTweetObject = tweets.find(t => t.id === tweetId);
if (existingTweetObject) {
// merge in changes to existing tweet
Object.assign(existingTweetObject, newData);
if (expectedTweetCount === newTweets.length) {
setTweets([...newTweets]); // force rerender with new info
}
} else {
// fresh tweet
tweets.push(newData);
if (expectedTweetCount === newTweets.length) {
setTweets(newTweets); // trigger initial render
}
}
});
userPrefNestedCancelListenerCallbacks.push(userPhotoCancelListenerCallback);
});
// return an Unsubscribe callback for this listener and its nested listeners.
return () => {
userPrefCancelListenerCallback();
userPrefNestedCancelListenerCallbacks.forEach(callIt);
}
})
);
});
// return an Unsubscribe callback for this listener and its nested listeners.
return () => {
tweetsCancelListenerCallback();
tweetsNestedCancelListenerCallbacks.forEach(callIt);
};
}, []);
Edit: Splitting the code in two components
Note: Changed limitToFirst(1) --> limit(1). Splitting the fetch logic in two components simplified the onSnapshot approach!
1.The Parent Component
useEffect(() => {
const tweetsUnsubscribeCallback = firestore
.collection('tweets')
.onSnapshot((tweetSnapshot) => {
const mappedtweets = tweetSnapshot.docs.map((tweetDoc) => {
return {
id: tweetDoc.id,
...tweetDoc.data(),
};
});
setTweets(mappedtweets);
});
return () => tweetsUnsubscribeCallback();
}, []);
2.The Child Component: Tweet
useEffect(() => {
// Helper Function
const unSubscribe = (unsub) => unsub();
//------------getting the AUTHOR USER PREFERENCE
const userPrefNestedUnsubscribeCallbacks = [];
const userPrefUnsubscribeCallback = firestore
.collection('user_preferences')
.where('uid', '==', tweet.uid)
.limit(1)
.onSnapshot((userPrefSnapshot) => {
userPrefNestedUnsubscribeCallbacks.forEach(unSubscribe); // cancel nested subscriptions
userPrefNestedUnsubscribeCallbacks.length = 0; // clear the array, but don't lose the reference
//------------getting the AUTHOR PHOTO
const userPhotoUnsubscribeCallback = firestore
.collection('user_photos')
.where('id', '==', tweet.uid)
.limit(1)
.onSnapshot((userPhotoSnapshot) => {
// Taking the whole data I need from all snapshots
setAuthor({
author: userPrefSnapshot.docs[0].data().username,
authorColor: userPrefSnapshot.docs[0].data().color,
authorPhoto: userPhotoSnapshot.docs[0].data().photoURL,
});
});
userPrefNestedUnsubscribeCallbacks.push(userPhotoUnsubscribeCallback);
});
return () => {
userPrefUnsubscribeCallback();
userPrefNestedUnsubscribeCallbacks.forEach(unSubscribe);
};
}, []);
Basically, you've pushed the promises to your promise array in the state they were before you you processed their data. You want to make use of the Promise.all(docs.map((doc) => Promise<Result>)) pattern here where each document should return a single Promise containing its final result. This then means that the Promise.all will resolve with Result[].
Note: If inside a Promise you are mutating a variable outside of the Promise (e.g. pushing to an array), that is generally a sign that you are doing something wrong and you should rearrange your code.
Here's a quick example of throwing this together:
useEffect(() => {
let unsubscribed = false;
//------------getting the TWEETS with onSnapshot()-------------
const cancelSuscription = firestore
.collection('tweets')
.onSnapshot((snapshot) => {
const tweetsMappedPromises = snapshot.docs.map((doc) => {
let tweetAndAuthor;
const tweetMappped = {
text: doc.data().text,
likes: doc.data().likes,
email: doc.data().email,
created: doc.data().created,
uid: doc.data().uid,
id: doc.id,
};
//------------getting the AUTHOR USER PREFERENCES with .get()-------------
const userPreferencePromise = firestore
.collection('user_preferences')
.where('uid', '==', tweetMappped.uid)
.limitToFirst(1)
.get()
.then((prefDocQuerySnapshot) => {
const firstPrefDoc = photoDocQuerySnapshot.docs[0];
const { username, color } = firstPrefDoc.data();
return { username, color };
});
//------------getting the AUTHOR PHOTO with .get()-------------
const userPhotoPromise = firestore
.collection('user_photos')
.where('id', '==', tweetMappped.uid)
.limitToFirst(1)
.get()
.then((photoDocQuerySnapshot) => {
const firstPhotoDoc = photoDocQuerySnapshot.docs[0];
return firstPhotoDoc.get("photoURL");
});
//--------------------assemble this result---------------------
return Promises.all([userPreferencePromise, userPhotoPromise])
.then(([authorPreference, authorPhoto]) => {
return {
...tweetMappped,
author: authorPreference.username,
authorColor: authorPreference.color,
authorPhoto: authorPhoto.photoURL,
};
});
});
Promise.all(tweetsMappedPromises)
.then((tweetsMapped) => {
if (unsubscribed) return; // ignore result, dealing with out of date data
setTweets(tweetsMapped);
})
.catch((err) => {
if (unsubscribed) return; // ignore result, dealing with out of date data
// important! handle errors
});
});
return () => {
unsubscribed = true;
cancelSuscription();
}
}, []);
Notes:
You may benefit from using async/await syntax here instead.
On new onSnapshot calls, snapshot.docChanges() can be used to make it more efficient and speed up rerenders by only updating the entries that have changed (e.g. added/removed/modified). You would use setTweets(previousTweetsMapped => /* newTweetsMapped */) for this.