React: how to initiate a variable with API call response - javascript

I am trying to create a table that uses an api to access data from a database.
import { COLUMNS } from './ActivityColumns.js'
import { getTable } from "./ApiCall.js"
export const SortingTable = () => {
const columns = useMemo(() => COLUMNS, [])
const [aaa, setData] = useState([])
const getData = () => {
getTable('activity').then(result => {
console.log('returning result')
return result;
})
}
const tableInstance = useTable({
columns: COLUMNS,
data: getData()
},
useSortBy)
}
The result in getData() is the expected one, an array of Object dicts [ {..}, {..}, ..., {..}]. The problem is that when tableInstance tries to set the data as the result of getData(), I get the following error: useTable.js:591 Uncaught TypeError: Cannot read properties of undefined (reading 'forEach')
The exact line that causes the error is from useTable.js:
data.forEach((originalRow, rowIndex) =>
accessRow(originalRow, rowIndex, 0, undefined, rows)
)
Before using an API I used a json file that has the same structure as the API's response: [{..}, {..}, {..}]. This one worked.
What goes wrong when trying to initiate with an API and how can I fix this error?
Edit 1: I tried to declare getData inside the hook
export const SortingTable = () => {
const columns = useMemo(() => COLUMNS, [])
const getData = () => {
const [data, setData] = useState(null);
const getData = () => {
getTable('activity').then(result => {
console.log('returning result')
console.log(result)
setData(result);
})
}
getData();
return data;
}
const tableInstance = useTable({
columns: COLUMNS,
data: getData
},
useSortBy)
const { getTableProps, getTableBodyProps, headerGroups, footerGroups, rows, prepareRow } = tableInstance
}

Your getData function does not have a return. This means calling getData() would give you undefined.
Is useTable OK to work with an asynchronous function?
If so, just return something from getData.
If it expects its synchronous data you might need to put the results of getData into state so that it gets populated once available which in turn causes a rerender of your component and calling useTable again.
import { COLUMNS } from './ActivityColumns.js'
import { getTable } from "./ApiCall.js"
export const SortingTable = () => {
const columns = useMemo(() => COLUMNS, [])
const [aaa, setData] = useState([])
useEffect(() => {
getData(); // <-----
}, []);
const getData = () => {
getTable('activity').then(result => {
setData(result); // <-----
})
}
const tableInstance = useTable({
columns: COLUMNS,
data: aaa // <-----
},
useSortBy)
}
The reason why your JSON was loaded without issue and your fetching is problematic is probably because you just imported/required the JSON which is synchronous while getTable obviously returns a thenable/Promise.

getData doesn't return anything. So effectively you're passing useTable an object {columns: COLUMNS, data: undefined}.
One way to fix this is to declare getData inside your hook, and use state to save the result of the API call:
const useTable = () => {
const [data, setData] = useState(null);
const getData = () => {
getTable('activity').then(result => {
console.log('returning result')
setData(result);
})
}
getData();
// do whatever you want to do here with COLUMNS
return data;
}

Related

Return value firestore onSnapshot in react

i have a onSnapshot query in a function:
//firebaseutil.js
export async function getShorts(uid) {
const q = query(collection(db, 'shorted'), where('owner', '==', uid));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const urls = [];
querySnapshot.forEach((doc) => {
urls.push({
url: doc.data().url,
shorturl: doc.data().shorturl,
hits: doc.data().hits,
});
});
console.log(urls);
return urls;
});
}
Which correctly logs the data, and relog it if i change it on the firestore collection (as expected)
i am trying to access these data from a user dashboard this way:
//dashboard.js
import { getShorts } from '../lib/fbutils';
import { useEffect, useState } from 'react';
export default function Dashboard() {
const [myurls, setUrls] = useState([]);
useEffect(() => {
const fetchShorts = async () => {
if (user) {
const urls = await getShorts(user.uid);
setUrls(urls);
console.log(myurls);
console.log(urls);
}
};
fetchShorts();
}, []);
user.id is correctly set, but both urls and myurls are logged as undefined (i was thinking at least for a Promise pending)
what am i doing wrong? i usually use this pattern to retrieve data, but it's my first time i get data from a firestore subscription
The onSnapshot() does not return a promise and your getShorts() function returns before the data is received. You can return a promise from that function as shown below:
let fetched = false;
export function getShorts(uid) {
return new Promise((resolve, reject) => {
const q = query(collection(db, 'shorted'), where('owner', '==', uid));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const urls = querySnapstho.docs.map((d) => ({ id: d.id, ...d.data() }))
if (!fetched) {
// URLs fetched for first time, return value
fetched = true;
resolve(urls);
} else {
// URLs fetched already, an update received.
// TODO: Update in state directly
}
})
})
}
This should return all the URLs when you call the function await getShorts(user.uid); but for the updates received later, you'll have to update them in the state directly because the promise has resolved now.

How to wait async data to start sync function

I get some data from an api call and set them in a state. Then I use this state variable in another function to filter some data. When the user opens the interface for the first time the data doesnt show because the sync function gets the empty data from the state.
Here is the code :
const [evQuestion, setEvQuestion] = useState();
const [answers, setAnswers] = useState();
const getEvaluationsQuestionsByOrganizations = async (evalId) => {
const response = await apiStandarts.get(`/evaluation-questions?organization_evaluation=${evalId}`);
setEvQuestion(response.data);
};
const evAnswers = () => {
const evAnswers = questions.map(q => {
return evQuestion?.map(ev => {
return q.question_options.find(i => i.id === ev.questOptionId)
});
});
const filterAnswers = evAnswers.map(q => {
return q?.filter(Boolean)
})
const answersToObject = filterAnswers.map(item => {
return convertArrayToObject(item)
});
const arr = {...answersToObject}
const obj2 = Object.fromEntries(
Object.entries(arr).map(([key, value]) => [key, value])
)
const obj3= Object.values(obj2).map(item => {
return {[item.question]: {...item}}
})
const savedAnswers = convertArrayToObject(obj3);
console.log(savedAnswers)
setAnswers(savedAnswers)
}
useEffect(() => {
getEvaluationsQuestionsByOrganizations();
evAnswers();
}, [])
I've tried to wrap the evAnswers function in a settimeout function but with no luck. How can I achieve this, any ideas?
Try adding another useEffect hook that depends on evQuestion state.
useEffect(() => {
getEvaluationsQuestionsByOrganizations();
}, []);
useEffect(() => {
evAnswers();
}, [evQuestion]);
the function getEvaluationsQuestionsByOrganizations(..) is defined as async function, but you are using it synchronously, in that case you should call your codes as below:
useEffect(() => {
const fetchedDataAPI = async () => {
return await getEvaluationsQuestionsByOrganizations();
};
fetchedDataAPI
.then(res => { evAnswers();})
.catch(err => {..});
;
}, []);

Why useState variable gets undefined inside try catch statement in an asynchrone function?

I create a hook that manages the state of a single object with fetch to api. This hook exposes function to interact with this object.
// the hook
const useMyHook = () => {
const [myObject, setMyObject] = useState(null);
useEffect(() => {
const fetchData = async () => {
const data = await fetchSomething();
setMyObject(data);
}
fetchData();
}, []);
const updateMyObject = async () => {
console.log(myObject); // log : { ... }
try {
console.log(myObject); // log : undefined
// ...
} catch(err) {
// ...
}
};
return {
updateMyObject,
myObject
};
};
Then i use this hook inside a component and trigger updateMyObject() with a button.
// the component
const MyComponent = () => {
const { myObject, updateMyObject } = useMyHook();
return (
<button onClick={updateMyObject}>
Click me
</button>
);
};
How is this possible that before the try catch block the log is clean and inside the block i get undefined ?
I think this gonna work
useEffect(() => {
const fetchData = async () => {
const data = await fetchSomething();
setMyObject(data);
}
If(!myObject)
fetchData();
}, [myObject]);
Your code is perfectly alright !! There could be a problem in the fetchSomething() method. Ideally, it should return data, but it's not doing the same job.
Here is a small example. You can give it a try.
const fetchSomething = async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
).then((res) => res.json());
return response;
};

React Native memory leak error after updating to Firebase version 9 onValue

I'm going through to update my code for the Firebase version 9 modular form. I am now using onValue. From what I'm reading it returns a function that removes the listener. But I'm still not doing it right because although it functions well at first, when I change the database on the backend with the app open I get the "can't perform a react state update on an unmounted component" error when I'm in a different app screen. See old and new code below please.
OLD CODE:
useEffect(() => {
loadListings();
},[]);
const loadListings = async () => {
setLoading(true);
updateInput('');
let testData = [];
let searchData = [];
db.ref('deals').once('value', (snapshot) =>{
snapshot.forEach((child)=>{
testData.push({
id: child.key,
title: child.val().hasOwnProperty('title') ? child.val().title : 'NA',
})
searchData.push(
child.val().title
)
})
})
.then(()=>{
checkMessages(testData);
setLoading(false);
})
.catch((error) => Sentry.Native.captureException('Error MessagesScreen function loadListings 1 ' + error));
}
NEW CODE:
useEffect(() => {
loadListings();
},[]);
const loadListings = async () => {
setLoading(true);
updateInput('');
const dbRef = ref(db, 'deals');
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
let storeData = filterStores;
snapshot.forEach((childSnapshot)=>{
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().hasOwnProperty('title') ? childSnapshot.val().title : 'NA',
})
})
checkMessages(testData);
setLoading(false);
})
}
After receiving answer below I changed the useEffect to this instead and now it works:
useFocusEffect(
React.useCallback( () => {
async function fetchData() {
// You can await here
const response = await loadListings();
// ...
return () => response();
}
fetchData();
}, [])
);
You mentioned the unsubscribe function returned from onValue. In order to call it, I think you'll want to grab it from the invocation and then call it on some navigation state change.
Assuming you're using React Navigation, it might look something like this (using the useFocusEffect
import { useFocusEffect } from '#react-navigation/native';
function YourComponent() {
const [loading, setLoading] = React.useState(false)
useFocusEffect(
React.useCallback(async () => {
const unsubscribe = await loadListings();
return () => unsubscribe();
}, [])
);
const loadListings = async () => {
setLoading(true);
updateInput('');
const dbRef = ref(db, 'deals');
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
let storeData = filterStores;
snapshot.forEach((childSnapshot)=>{
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().hasOwnProperty('title') ? childSnapshot.val().title : 'NA',
})
})
checkMessages(testData);
setLoading(false);
})
}
return <View />;
}
Also don't forget to either use async/await for your asynchronous loadListings function, or use .then(). Otherwise you'll be working with an unresolved promise.
I also found a related StackOverflow question that helped me get to this answer. Maybe that'll be of some use to you.

How to return an array received from fetching api data in a .then statement?

I'm trying to export an array inside a .then statement but its not working. I have no clue how to make it work otherwise. Actually I'm just trying to set my initial state in redux to this static data I am receiving from the movie database api.
import { API_URL, API_KEY } from '../Config/config';
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
]
Promise.all(urls.map(items => {
return fetch(items).then(response => response.json())
}))
.then(arrayOfObjects => {
var arr1 = arrayOfObjects[0].results;
var arr2 = arrayOfObjects[1].results;
export var movieData = arr1.concat(arr2);
}
)
You can try with a function. like this:
import { API_URL, API_KEY } from '../Config/config';
export const getMovies = () => {
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
]
const promises = urls.map(url => {
return new Promise((reject, resolve) => {
fetch(url).then(res => res.json())
.then(res => resolve(res.results))
})
})
return Promise.all(promises)
}
// other file
import {getMovies} from 'YOUR_API_FILE.js';
getMovies().then(moviesArr => {
// your business logics here
})
It's not clear where this code is in relation to your state/reducer, but ideally you should be using action creators to deal with any API calls and dispatch state updates, and those action creators can be called from the component.
So, initialise your state with an empty array:
const initialState = {
movies: []
};
Set up your reducer to update the state with MOVIES_UPDATE:
function reducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case 'MOVIES_UPDATE': {
return { ...state, movies: payload };
}
}
}
You can still use your function for fetching data:
function fetchData() {
return Promise.all(urls.map(items => {
return fetch(items).then(response => response.json());
}));
}
..but it's called with an action creator (it returns a function with dispatch param), and this action creator 1) gets the data, 2) merges the data, 3) and dispatches the data to the store.
export function getMovies() {
return (dispatch) => {
fetchData().then(data => {
const movieData = data.flatMap(({ results }) => results);
dispatch({ type: 'MOVIES_UPDATE', payload: movieData });
});
}
}
And it's called from within your component like so:
componentDidMount () {
this.props.dispatch(getMovies());
}
You can modify the code as below:
import { API_URL, API_KEY } from '../Config/config';
let movieData='';
exports.movieData = await (async function(){
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
];
const arrayOfObjects = await Promise.all(urls.map(items => {
return fetch(items).then(response => response.json())
}));
return arrayOfObjects[0].results.concat(arrayOfObjects[1].results);
})();

Categories

Resources