How to get asynchronous data into a global array in React.js? - javascript

I want to insert asynchronous data from Firestore into React elements. I have a couple of functions that handle the flow.
getEvents() is an asynchronous function that returns an array of objects that is my data from Firestore.
const getEvents = async() => {
try {
const data = query(collection(db, id));
const events = [];
const snapshot = await getDocs(data);
snapshot.forEach((doc) => events.push(doc.data()));
return events;
} catch(error) {
console.error(error);
}
}
This function is referenced in receiveEvents() where I take the returned data and put it into a global array in order to use it in the DOM.
let userEvents = [];
const receiveEvents = () => {
getEvents()
.then(result => userEvents = result)
.catch(error => console.error(error));
This function is used in displayEvents() to paste the returned data into the desired element. The function is called upon a button click.
const displayEvents = () => {
try {
const btnContainer = document.querySelector(".btn-container");
ReactDOM.render(<AppNavigation />, btnContainer);
receiveEvents().then(() => { return userEvents });
} catch(error) {
console.error(error);
}
}
I get an error index.js:1 TypeError: Cannot read properties of undefined (reading 'then') # displayEvents.
These functions use the logged user ID to access the right directory in the database. I retrieve the ID in a given function declared at the top of the file.
let id = null;
const getUserId = () => {
try {
console.log("getUserId()");
return auth.currentUser.uid;
} catch(error) {
console.error(error);
}
}
The <AppNavigation/> component returns a div, namely:
<div className="arrow-navigation-container">
<button className="arrow-btn"><span className="icon arrow">arrow_back</span></button>
<button className="arrow-btn"><span className="icon arrow">arrow_forward</span></button>
</div>
What can I do to get the asynchronous data into the userEvents array, so I can show it to the user upon request?

I would use hooks for this personally:
useState for storing the data
useEffect for triggering the fetching of the data
useContext for providing the data for whatever component that needs the data.
Examples how to use those hooks: https://reactjs.org/docs/hooks-reference.html#usestate
The useState would go into the same component as the useContext Provider, usually in example apps it's in component. The component which needs the data needs to be a child of the Context.Provider, but it doesn't need to be a direct child.
useEffect goes also to the same component, and with the dependencies you can decide if you want to fetch the data only once or for example when that ID changes.

Related

How to return a derived function that use fetched data from db based on value of another writable value in svelte/svelte kit?

I am trying to get my language table's data from db in store.js and modify that data based on the language user selected. So the selected language is a writable variable and I need to get a derived function that returns the modified data. Here is my code in store.js
import { writable, derived } from 'svelte/store';
export async function getData(endpoint){
try {
const res = await axios.get(`${baseAPI}${endpoint}`);
return res.data;
} catch (e) {
return e;
}
}
function getTypography(lang){
let typography;
try {
const texts = getData('/language');
texts.then((data)=>{
typography = data.payload;
console.log(typography);
return filterTypography(typography, lang)
}).catch((e)=>{
console.log('error', e);
})
} catch (error) {
console.log('error', error);
}
}
function filterTypography(typography, lang){
let textarray=[];
typography.forEach((element, index) => {
textarray[index]={
'keyword':element.keyword,
'value':element[lang]
}
});
return textarray
}
export const key = writable('en')
export const updateKey = (lang) => {
key.set(lang)
}
export const data = derived(key, $key => getTypography($key));
Here is the code in my +page.svelte
<script>
import { key, data, updateKey } from '$lib/store.js'
function changeLang(lang){
console.log("clicked with", lang);
updateKey(lang)
}
$: console.log($key)
</script>
<h1>{$data}</h1>
<button on:click={() => changeLang('dk')} >Change to DK</button>
<button on:click={() => changeLang('en')}>Change to EN</button>
I am getting 'undefined' data. I am just trying to print out the data variable for testing. Please note that, the console log that I am printing after getting the data from the API endpoint is showing the data successfully. Also, another thing, if I just use a function in store instead of the getTypography function, that returns different static array based on the language chosen, it works perfectly fine. So to my understanding the issue might be getting the data properly from db. What can I do here?
The derived store makes no sense in this context because the data is updated asynchronously. The problem being that getTypography is not returning anything, but being used in data as if that were the case.
Just make data a writable and update that when the language changes. I.e. use something like:
key.subscribe($key => updateData($key))
Where the updateData function is analogous to getTypography but sets the data store.
Something like:
const data = writable([]);
async function updateData(lang) {
try {
const languageData = await getData('/language');
const typography = languageData.payload;
data.set(filterTypography(typography, lang));
}
catch (error) {
console.log('error', error);
}
}
(You may want to add additional logic to invalidate previous requests to prevent race conditions.)
If data for all languages is requested at once, you can update a base store asynchronously and use a derived store just for the filtering. It should be based on both the language (key) and the store that is updated with all the language data.
An example of the principle:
import { writable, derived } from 'svelte/store';
const key = writable('en');
const allData = writable({});
const data = derived([allData, key], ([$allData, $key]) => {
return $allData[$key]; // Filter data based on language
});
REPL
You could also rewrite getTypography to actually return the promise it creates internally, but then you would have to use something like {#await} everywhere you use data.
async function getTypography(lang) {
try {
const data = await getData('/language');
const typography = data.payload;
return filterTypography(typography, lang);
}
catch (error) {
console.log('error', error);
// If you do not return anything here, the awaited store will be `undefined`
}
}

How to access data outside of a Promise

I'm making a react app that sends an API call to OpenWeather to get the weather data for a city (specified by the user). Here's what the request for that call looks like:
async function getAPI() {
const apiCall = await axios.get(apiLink).then(res => {
res = {
temp : res.data.main.temp - 273.15,
weatherIcon : res.data.weather[0].icon,
windSpeed : res.data.wind.speed
}
return res
});
return apiCall
}
const weatherData = getAPI()
Notice that I try to store the data I want from the API response in a variable called weatherData. That way I can simply call that variable whenever I need, heres an example of HTML code that uses this variable:
<p>
temperature is {weatherData.temp} Celcius
</p>
This results in weatherData.temp simply not showing up on the browser side for some reason. A console.log(weatherData) prints this in the console:
Promise {<pending>}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Object
temp: 29.53
weatherIcon: "04d"
windSpeed: 1.59
[[Prototype]]: Object
How do I extract the data from the promise in a way that allows me to easily refer to said data for use in HTML code?
Answer below is if you are using functional components and react hooks.
You can can go two directions:
Using a try catch block:
const fetchWeather = async () => {
try {
const res = await axios.get(apiLink);
console.log(res);
setWeather(res.data); //Im not sure what the exact response is, but you can access the keys you need.
// you can then set the data you need to your state to render it.
} catch (error) {
// handle error
}
}
Or you can use .then .catch
const fetchWeather = async () => {
axios.get(apiLink)
.then((res) => {
setWeather(res.data); //Im not sure what the exact response is, but you can access the keys you need.
// set the data you need from the respones to your state.
})
.catch((err) => {
// handle error
})
}
In both cases you can just call the function in your useEffect hook.
useEffect(() => {
fetchWeather()
}, [])
In general my preference goes to set the response you get from the Api into the local state (meaning the state of your page/component). And then rendering the state to your jsx.
So if you are using react hooks, your state could look like this:
const [weather, setWeather] = useState({});
Last Edit:
Finally you can just refer to your state within your jsx/html. Assuming your weather state looks like this:
{
temp: '50 degrees'
}
In your JSX you can just refer to it this way:
<>
<div>{weather.temp}</div>
</>

How to return something conditionally based off the result of a firebase query? [duplicate]

I have two tables in Firebase: Vouchers & ClaimedVouchers. I am trying to display the vouchers that do not appear in the ClaimedVouchers table. So, I have a query that gets all of the vouchers, then another that checks if they're claimed and if it's claimed or not the function should return either true or false:
This is isClaimedAPI.js
export default () => {
var result;
async function isClaimed(voucher, user) {
var db = Firebase.firestore();
console.log("voucher");
await db
.collection("ClaimedVoucher")
.where("voucherID", "==", voucher)
.where("userID", "==", user)
.get()
.then(function (querySnapshot) {
if (querySnapshot.empty === false) {
result = true;
} else {
result = false;
}
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
console.log(result, "this is result");
return result;
//Call when component is rendered
useEffect(() => {
isClaimed().then((result) => setResult(result));
}, []);
return [isClaimed];
And then in my main function:
var user = Firebase.auth().currentUser;
var uid = user.uid;
const [getVouchers, voucherList, errorMessage] = getVouchersAPI(ID); //List of vouchers to be checked
const [isClaimed] = isClaimedAPI();
return(
<ScrollView>
{voucherList.map((item, index) => {
var voucher = item;
var isVoucherClaimed = isClaimed(voucher.voucherID, uid);
console.log("this is result, ", isVoucherClaimed);
if (
isVoucherClaimed === false
) {
return <Text>{item.name}<Text>
}
})}
</ScrollView>
);
Now nothing happens and I receive the following warning: [Unhandled promise rejection: FirebaseError: Function Query.where() requires a valid third argument, but it was undefined.] but I think this is unrelated to the issue.
Your isClaimed is an async function, meaning that it returns a promise - or a delayed result. If you want to wait for the result when calling isClaimed, you'll need to use await:
await isClaimed(voucher.voucherID, uid);
console.log(result);
This most likely isn't possible in a render method though, which is why (as Asutosh commented) you'll have to store the result in the state, and then use the state in your render method.
So the setup you need is:
Start the loading of all your data in componentDidMount or with useEffect.
When the data is loaded, put it in the state with setState or a state hook.
Use the data in your render method.
For a few examples of this, see:
Firebase switch header option with onAuthStateChanged
React + Firestore : Return a variable from a query
How to render async data in react + firestore?
Just to highlight how you can use isClaimed as hoook and set state calling a async function inside it. Later use the above hook in a react component. Please follow below sanbox.
https://codesandbox.io/s/confident-cray-4g2md?file=/src/App.js

How to use useEffect inside map function?

I have two tables in Firebase: Vouchers & ClaimedVouchers. I am trying to display the vouchers that do not appear in the ClaimedVouchers table. So, I have a query that gets all of the vouchers, then another that checks if they're claimed and if it's claimed or not the function should return either true or false:
This is isClaimedAPI.js
export default () => {
var result;
async function isClaimed(voucher, user) {
var db = Firebase.firestore();
console.log("voucher");
await db
.collection("ClaimedVoucher")
.where("voucherID", "==", voucher)
.where("userID", "==", user)
.get()
.then(function (querySnapshot) {
if (querySnapshot.empty === false) {
result = true;
} else {
result = false;
}
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
console.log(result, "this is result");
return result;
//Call when component is rendered
useEffect(() => {
isClaimed().then((result) => setResult(result));
}, []);
return [isClaimed];
And then in my main function:
var user = Firebase.auth().currentUser;
var uid = user.uid;
const [getVouchers, voucherList, errorMessage] = getVouchersAPI(ID); //List of vouchers to be checked
const [isClaimed] = isClaimedAPI();
return(
<ScrollView>
{voucherList.map((item, index) => {
var voucher = item;
var isVoucherClaimed = isClaimed(voucher.voucherID, uid);
console.log("this is result, ", isVoucherClaimed);
if (
isVoucherClaimed === false
) {
return <Text>{item.name}<Text>
}
})}
</ScrollView>
);
Now nothing happens and I receive the following warning: [Unhandled promise rejection: FirebaseError: Function Query.where() requires a valid third argument, but it was undefined.] but I think this is unrelated to the issue.
Your isClaimed is an async function, meaning that it returns a promise - or a delayed result. If you want to wait for the result when calling isClaimed, you'll need to use await:
await isClaimed(voucher.voucherID, uid);
console.log(result);
This most likely isn't possible in a render method though, which is why (as Asutosh commented) you'll have to store the result in the state, and then use the state in your render method.
So the setup you need is:
Start the loading of all your data in componentDidMount or with useEffect.
When the data is loaded, put it in the state with setState or a state hook.
Use the data in your render method.
For a few examples of this, see:
Firebase switch header option with onAuthStateChanged
React + Firestore : Return a variable from a query
How to render async data in react + firestore?
Just to highlight how you can use isClaimed as hoook and set state calling a async function inside it. Later use the above hook in a react component. Please follow below sanbox.
https://codesandbox.io/s/confident-cray-4g2md?file=/src/App.js

Setting a useEffect hook's dependency within`useEffect` without triggering useEffect

Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
​
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
​
I hope the above makes sense.
​
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
​
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).

Categories