So this is really strange
this is my code when it works
useEffect(() => {
// Alleen preview
async function getTracksData() {
let TrackData = await firebase
.firestore()
.collection('TrackScreen')
.doc('LeerjarenData')
.get();
if (!TrackData.exists) {
console.log('Geen data')
} else {
let TrackDatav2 = TrackData.data();
setTrackScreenData(TrackDatav2)
}
} getTracksData()
// SNAPSHOT USER DATA
db.collection("users").doc(currentUserUID)
.onSnapshot((doc) => {
setUserData(doc.data());
});
}, [])
console.log(trackscreenData.data)
this works perfectly but when i change my console to
console.log(trackscreenData.data[0]
it gives me this error
TypeError: undefined is not an object (evaluating 'trackscreenData.data[0]')
then when i change my console again to
console.log(trackscreenData.data)
it works and when i change i back to
console.log(trackscreenData.data[0])
and save the changes it gives me the data i want
what am i doing wrong?
In short, you are trying to use the data from your asynchronous call before its ready.
To handle the case where your data hasn't finished loading, you should use:
console.log(trackscreenData && trackscreenData.data)
Based on the patterns in your code, you have these lines at the top of your component.
const { currentUserID } = useAuth(); // or similar
const [userData, setUserData] = useState();
const [trackscreenData, setTrackScreenData] = useState();
// NOTE: check for typos: "trackscreenData" !== "trackScreenData"
On the first render of your code, trackscreenData will be undefined because you haven't passed an initial state into the useState() method.
const [trackscreenData, setTrackScreenData] = useState(/* initial state */);
// `trackscreenData` initially set to `null`
const [trackscreenData, getTracksData] = useState(null);
// `trackscreenData` initially set to `[]`
const [trackscreenData, getTracksData] = useState([]);
// `trackscreenData` initially set to the result of the function,
// an array containing the elements 0 through 99. The function is
// only executed once to set the first value.
// trackscreenData = [0, 1, 2, 3, ... , 97, 98, 99];
const [trackscreenData, getTracksData] = useState(() => {
Array.from({length: 100})
.map((_, i) => i);
}
When React executes your code, any calls to setters returned from useState calls (e.g. setTrackScreenData) are queued. Only once your code has finished executing, are they evaluated and any new renders triggered.
const [count, setCount] = useState(0); // `count` is initially set to `0`
useEffect(() => {
if (count < 10) {
setCount(count + 1);
}
})
console.log(count);
console.log("rendered");
// Console logs:
// > "0"
// > "rendered"
// > "1"
// > "rendered"
// > ...
// > "9"
// > "rendered"
// > "10"
// > "rendered"
Your fetch of user data should be in its own useEffect call and should return its unsubscribe function:
useEffect(() => {
if (!currentUserUID) {
setUserData(null); // user not signed in
return;
}
// SNAPSHOT USER DATA
return db.collection("users") // <- note the return here
.doc(currentUserUID)
.onSnapshot((doc) => {
setUserData(doc.data());
});
}, [currentUserUID]);
useEffect(() => {
let disposed = false;
// Alleen preview
async function getTracksData() {
let TrackData = await firebase
.firestore()
.collection('TrackScreen')
.doc('LeerjarenData')
.get();
if (disposed) return; // component was removed, do nothing
if (!TrackData.exists) {
console.log('Geen data')
} else {
let TrackDatav2 = TrackData.data();
setTrackScreenData(TrackDatav2)
}
}
getTracksData()
.catch((err) => console.error(err)); // don't forget to handle errors!
return () => disposed = true; // ignore result if component disposed
}, []);
You are specifying the document ID by .doc(...) and hence you are getting a DataSnapshot as the response which is not an array.
If you were using queries like .where(...) then it would have returned a QuerySnapshot.
A QuerySnapshot contains zero or more DocumentSnapshot objects
representing the results of a query. The documents can be accessed as
an array via the docs property or enumerated using the forEach method.
The number of documents can be determined via the empty and size
properties.
So if you are fetching only a single document that way, you don't need to access the doc by [0] as it is not an array. You can simply access the data by dataSnapshot.data()
Although I can't see where is trackscreenData defined?
useEffect(() => {
// Alleen preview
async function getTracksData() {
let TrackData = await firebase
.firestore()
.collection('TrackScreen')
.doc('LeerjarenData')
.get();
if (!TrackData.exists) {
console.log('Geen data')
} else {
let TrackDatav2 = TrackData.data();
setTrackScreenData(TrackDatav2)
console.log(trackScreenData)
}
}
}, [])
Related
I'm pulling data from Firebase in getSubscriptions() which returns an array:
[
{
"price": 2000.01,
"product_name": "Awesome product"
},
{
"active": true,
"product_name": "Other product",
"Price": 599.99
}
]
I'm iterating through the array and I'm able to get a result but I'm having trouble getting that result to render.
I know the issue has something to do with async and waiting for the result to return but I'm stuck on how to apply those concepts to my issue here.
When I uncomment the line useEffect(() => setActiveNotifications(active), [active]), introducing the useState, the calls are put in an infinite loop calling the Firebase API ad infinitum.
export default function UserAccount(props) {
const [activeNotifications, setActiveNotifications] = useState()
function buildSubscriptions(userId) {
getSubscriptions(userId).then(active => {
console.log(JSON.stringify(active, null, 4)) // This returns output above
// useEffect(() => setActiveNotifications(active), [active]) // This will cause infinite loop
return (
active.map(product =>
<p key={product.product_name}>{product.product_name}</p>) // key defined some value to not throw warning
)
})
}
async function getSubscriptions(userId) {
const subPtrCollection = await getUserSubs(userId)
let a = []
await Promise.all(subPtrCollection.map(async docId => { // Promise.all used to execute commands in series
const docData = await getDocData(docId.product)
a.push(docData)
}))
return a
}
return (
...
<Box>{buildSubscriptions(user.uid)}</Box> // Not important where `user.uid` comes from
...
)
}
Infinite loop is because the method invoked in render method setting the state and that causes render. Try some thing like below 1) On change of uid, request for build 2) in when you receive the results from api save to state, this cause render 3) in render method used the state data.
export default function UserAccount(props) {
const [activeNotifications, setActiveNotifications] = useState();
// when ever uid changes, do the build subscriptions
useEffect(() => buildSubscriptions(user.uid), [user.uid]);
// get subscriptions and save to state
function buildSubscriptions(userId) {
getSubscriptions(userId).then((active) => setActiveNotifications(active));
}
async function getSubscriptions(userId) {
const subPtrCollection = await getUserSubs(userId);
let a = [];
await Promise.all(
subPtrCollection.map(async (docId) => {
// Promise.all used to execute commands in series
const docData = await getDocData(docId.product);
a.push(docData);
})
);
return a;
}
return (
<Box>
{activeNotifications.map((product) => (
<p key={product.product_name}>{product.product_name}</p>
))}
</Box>
);
}
I have a POST API call that I make on a button click. We have one large state object that gets sent as body for a POST call. This state object keeps getting updated based on different user interactions on the page.
function QuotePreview(props) {
const [quoteDetails, setQuoteDetails] = useState({});
const [loadingCreateQuote, setLoadingCreateQuote] = useState(false);
useEffect(() => {
if(apiResponse?.content?.quotePreview?.quoteDetails) {
setQuoteDetails(apiResponse?.content?.quotePreview?.quoteDetails);
}
}, [apiResponse]);
const onGridUpdate = (data) => {
let subTotal = data.reduce((subTotal, {extendedPrice}) => subTotal + extendedPrice, 0);
subTotal = Math.round((subTotal + Number.EPSILON) * 100) / 100
setQuoteDetails((previousQuoteDetails) => ({
...previousQuoteDetails,
subTotal: subTotal,
Currency: currencySymbol,
items: data,
}));
};
const createQuote = async () => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
const handleQuickQuote = useCallback(createQuote, [quoteDetails, loadingCreateQuote]);
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
// remove deal if present
if (quoteDetails.hasOwnProperty("deal")) {
delete quoteDetails.deal;
}
handleQuickQuote();
}
const generalInfoChange = (generalInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
tier: generalInformation.tier,
}
));
}
const endUserInfoChange = (endUserlInformation) =>{
setQuoteDetails((previousQuoteDetails) => (
{
...previousQuoteDetails,
endUser: endUserlInformation,
}
));
}
return (
<div className="cmp-quote-preview">
{/* child components [handleQuickQuote will be passed down] */}
</div>
);
}
when the handleQuickQuoteWithoutDeals function gets called, I am deleting a key from the object. But I would like to immediately call the API with the updated object. I am deleting the deal key directly here, but if I do it in an immutable way, the following API call is not considering the updated object but the previous one.
The only way I found around this was to introduce a new state and update it on click and then make use of the useEffect hook to track this state to make the API call when it changes. With this approach, it works in a weird way where it keeps calling the API on initial load as well and other weird behavior.
Is there a cleaner way to do this?
It's not clear how any children would call the handleQuickQuote callback, but if you are needing to close over in callback scope a "copy" of the quoteDetails details then I suggest the following small refactor to allow this parent component to use the raw createQuote function while children receive a memoized callback with the current quoteDetails enclosed.
Consume quoteDetails as an argument:
const createQuote = async (quoteDetails) => {
try {
setLoadingCreateQuote(true);
const result = await usPost(componentProp.quickQuoteEndpoint, quoteDetails);
if (result.data?.content) {
/** TODO: next steps with quoteId & confirmationId */
console.log(result.data.content);
}
return result.data;
} catch( error ) {
return error;
} finally {
setLoadingCreateQuote(false);
}
};
Memoize an "anonymous" callback that passes in the quoteDetails value:
const handleQuickQuote = useCallback(
() => createQuote(quoteDetails),
[quoteDetails]
);
Create a shallow copy of quoteDetails, delete the property, and call createQuote:
const handleQuickQuoteWithoutDeals = (e) => {
e.preventDefault();
const quoteDetailsCopy = { ...quoteDetails };
// remove deal if present
if (quoteDetailsCopy.hasOwnProperty("deal")) {
delete quoteDetailsCopy.deal;
}
createQuote(quoteDetailsCopy);
}
I have an issue with using Lodash + setInterval.
What I want to do:
Retrieve randomly one element of my object every 3 seconds
this is my object:
const [table, setTable]= useState ([]);
so I start with that:
const result = _.sample(table);
console.log(result);
console give => Object { label: "Figue", labelEn: "fig" }
But if a add :
const result = _.sample(table);
console.log(result.label);
console give => TypeError: result is undefined
Beside that I tried to add setInterval and also try with useEffect but or code crash or console give me two numbers every 3 second => 2,6,2,6 ......
Ciao, supposing that table object is correclty filled, you could use lodash and setInterval to get random object of table using useEffect and useRef hooks like:
const interval = useRef(null);
useEffect(() => {
if (!interval.current) {
interval.current = setInterval(() => {
const result = _.sample(table);
console.log(result.label);
}, 3000);
}
}, [table]);
Then to clean setInterval when component will be unmount you could use another useEffect in this way:
useEffect(() => {
return () => {
clearInterval(interval.current);
interval.current = null;
};
}, []);
EDIT
After your explanation, I found the cause of your problem. Before start setInterval you have to wait that table was filled with values (otherwise you get an error).
To solve that it's just necessary to put another condition on first useEffect (table.length > 0), and load data on second useEffect.
So the first useEffect becomes:
const interval = useRef(null);
useEffect(() => {
if (!interval.current && table.length > 0) {
interval.current = setInterval(() => {
const result = _.sample(table);
console.log(result.label);
}, 3000);
}
}, [table]);
And the second one:
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
return () => {
clearInterval(interval.current);
interval.current = null;
};
}, []);
Here the codesandbox updated.
The problem is that you access table before it is loaded.
You should either provide an initial value to that allows a successful do all your operations:
// use an array that has at least one element and a label as initial table value
const [table, setTable] = useState([{label: "default label"}]);
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
});
const result = _.sample(table);
console.log(result.label);
// ...
Or use an if or something alike to handle multiple scenarios:
// use an empty array as initial table value
const [table, setTable] = useState([]);
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
});
const result = _.sample(table);
// account for the initial empty table value by checking the result
// of _.sample which is `undefined` for empty arrays
if (result) {
console.log(result.label);
} else {
console.log("do something else");
}
// ...
If you fetch your data asynchronously you must think about what you want to use while the data is being fetched. The minimal thing to do is tell React to not render the component while the data is missing (being fetch).
const [table, setTable] = useState([]);
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
});
const result = _.sample(table);
if (!result) return null; // <- don't render if there is no result
console.log(result.label);
// ...
I am using firebase with react functional components, I am defining db object in useEffect and setting the object to state like this :
const [Db, setDb] = useState();
useEffect(() => {
const db = firebase.database();
setDb(db, checkStatus());
}, []);
const checkStatus = () => {
Db.ref("orders")
.once("value")
.then(snapshot => {
// do something
});
}
I have given checkStatus() as a callback to setDb() which I belive will get executed after setDb() changes the state (normally calling checkStatus() in useEffect() also gives the same error). Now while calling checkStatus(), I get the following error :
TypeError: Cannot read property 'ref' of undefined
I believe checkStatus() is getting executed before state changed, which leads to Db being undefined. I cannot figure out a way to call a function after the state value is set to something, and what are the best practices while calling functions and using state as such.
You can use useEffect
const [Db, setDb] = useState();
useEffect(() => {
const db = firebase.database();
setDb(db);
}, []);
useEffect(() => {
if(Db) {
Db.ref("orders")
.once("value")
.then(snapshot => {
// do something
});
}
}, [Db]);
Or you can set firebase in useState
const [Db, setDb] = useState(() => firebase.database());
useEffect(() => {
Db.ref("orders")
.once("value")
.then(snapshot => {
// do something
});
}, []);
Extract a custom hook, useDBStatus().
This custom hook abstracts any DB-related communication and just return what's relevant (i.e status).
function useDBStatus(ref) {
const db = firebase.database();
const [status, setStatus] = React.useState({});
React.useEffect(() => {
if (db) {
db(ref)
.once("value")
.then(snapshot => {
setStatus(/* something */);
});
}
return () => { /* close db connection */ };
}, [db, ref]);
return status;
}
function Component() {
const status = useDBStatus("orders");
// use status
}
I have a firebase cloud function as follows:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
})
.then(jobs => {
return jobs.forEach(job => {
var percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate); // add to team's feed
}
});
})
.catch(err => {
console.log("firebase got an error: ", err);
});
});
In function foo, I call a local non-cloud function getMatchedPercent which is defined as below:
const getMatchedPercent = (candidate, job) => {
console.log("In get percent: ", candidate, job);
// do something
};
The problem is when I checked job.val() in foo before calling getMatchedPercent, I can see valid data got printed from console for job.val(). When once get in getMatchedPercent, I tried to print job, it complains it's undefined.
Is there anything I missed? Why the information of job can be lost during calling a function? Thanks!
Your problem is caused by these lines:
const promises = [];
jobs.forEach(job => {
promises.push(job.val());
});
return Promise.all(promises);
job.val() returns an object (of the data) not a promise, so Promise.all() incorrectly interprets it as a resolved promise with no value. In your next block of code, the array jobs is an array of undefined values rather than the data you were expecting.
To fix this, you would instead return the array of values rather than using Promise.all().
const jobValues = [];
jobs.forEach(job => {
jobValues.push(job.val());
});
return jobValues;
But because no asyncronous work is taking place here you can flatten your Promise chain. By doing so, you will use less memory because you won't need an array containing of all of your job.val() objects at once.
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const promises = []; // will contain any team feed update promises
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
promises.push(
admin
.database()
.ref("feeds")
.child(job.feedId)
.child("upcomingWeek")
.push(candidate) // add to team's feed
);
}
});
return Promise.all(promises);
})
.catch(err => {
console.log("Failed to update team feeds: ", err);
});
});
However, this still has another problem where some of the feed updates may succeed and others may fail which leaves your database in an unknown state. So instead you might want to consider writing to the database atomically (all data is written, or nothing at all).
This could be achieved using:
exports.foo = functions.database
.ref("/candidates/{jobTrack}/{candidateId}")
.onCreate((snap, context) => {
const candidate = snap.val().candidate;
const jobTrack = context.params.jobTrack;
const jobsRef = admin.database().ref("jobs");
return jobsRef
.child(jobTrack)
.once("value")
.then(jobs => {
const pendingUpdates = {}; // "path: value" pairs to be applied to the database
const feedsRef = admin.database().ref("feeds");
jobs.forEach(jobSnapshot => { // This is DataSnapshot#forEach
const job = jobSnapshot.val();
const percent = getMatchedPercent(candidate, job);
if (percent >= 0.9) {
const pushId = feedsRef.push().key; // push() without arguments doesn't write anything to the database, it just generates a new reference with a push ID we can use.
const path = job.feedId + "/upcomingWeek/" + pushId;
pendingUpdates[path] = candidate; // queue add to team's feed
}
});
// apply all updates in pendingUpdates object,
// relative to feedsRef as an all-or-nothing operation.
// e.g. pendingUpdates["feed001/upcomingWeek/9jksdfghsdjhn"] = "someUserId"
// will be written to "feeds/feed001/upcomingWeek/9jksdfghsdjhn"
return feedsRef.update(pendingUpdates); // commit changes
})
.catch(err => {
console.log("Failed to apply all feed updates: ", err);
});
});