I'm using react-query to make two separate queries in the same React component. I originally tried using two useQuery hooks:
export default function Component() {
const [barData, setBarData] = useState();
const [lineData, setLineData] = useState();
const { error: errorBar, isLoading: loadingBar } = useData(
"barQuery",
"BAR_TEST_SINGLE",
setBarData
);
const { error: errorLine, isLoading: loadingLine } = useData(
"lineQuery",
"LINE_TEST",
setLineData
);
const isLoading = loadingLine && loadingBar;
const error = errorLine && errorBar;
if (isLoading) return <LoadingSpinner title={title} />;
if (error)
return <InvalidStateAPI description={error.message} title={title} />;
return (
<>
<Line data={lineData} />
<Bar data={barData} />
</>
);
}
Here's the content of useData, which is imported from another file:
export function useData(queryId, chartId, setData) {
return useQuery(
queryId,
() =>
fetchData(chartId, "models").then((resp) => {
setData(resp.Item.data);
})
);
}
fetchData is a function that queries an AWS DDB table. I'm happy to post that function, but I'm currently excluding it for brevity.
Rendering Component results in this error: TypeError: Cannot read properties of undefined (reading 'filter'). I suspect this error is thrown because a component doesn't get its data in time for rendering. I thought this would be solved by adding const isLoading = loadingLine && loadingBar;, as suggested in this post, but it did not.
Finally, on the recommendation of this SO answer, I decided to use useQueries. The documentation is quite sparse, but it recommends the following format:
const results = useQueries([
{ queryKey: ['post', 1], queryFn: fetchPost },
{ queryKey: ['post', 2], queryFn: fetchPost },
])
I modified my original code to the following:
const results = useQueries([
{
queryKey: ["post", 1],
queryFn: useData2("barQuery", "BAR_TEST_SINGLE", setBarData),
},
{
queryKey: ["post", 2],
queryFn: useData2("lineQuery", "LINE_TEST", setLineData),
},
]);
but now I'm getting this error: TypeError: _this2.options.queryFn is not a function.
Am I formatting my query incorrectly? How can I fix it? Alternatively, are there other ways to run different queries using react-query in the same component?
No, this is not the correct syntax for useQueries. You can't pass a useQuery hook in as queryFn - the queryFn needs the function that fetches the data, in your case, that would be fetchData(chartId, "models").
The root cause of your initial problem however seems to be that your condition only waits until one of the queries has finished loading:
const isLoading = loadingLine && loadingBar;
if loadingLine is false and loadingBar is true, this condition will yield false and you will thus not display a loading spinner anymore. If you want to wait until all queries have finished loading, that would be:
const isLoading = loadingLine || loadingBar;
lastly, I'd like to point out that copying data from react-query to local state is not necessary. react-query will return the result returned from the queryFn as the data field. My implementation would look something like that:
export function useData(queryId, chartId) {
return useQuery(
[queryId, chartId],
() => fetchData(chartId, "models")
);
}
const { error: errorBar, isLoading: loadingBar, data: barData } = useData(
"barQuery",
"BAR_TEST_SINGLE",
);
const { error: errorLine, isLoading: loadingLine, data: lineData } = useData(
"lineQuery",
"LINE_TEST",
);
const isLoading = loadingLine || loadingBar;
or, alternatively, with useQueries:
const results = useQueries([
{
queryKey: ["barQuery", "BAR_TEST_SINGLE"],
queryFn: () => fetchData("BAR_TEST_SINGLE", "models")
},
{
queryKey: ["lineQuery", "LINE_TEST"],
queryFn: () => fetchData("LINE_TEST", "models")
},
]);
const isLoading = results.some(result => result.isLoading)
#TKDo, Your solution only works with react-query versions till "4.0.0-alpha.1", the issue will crop up again, if we use any latest version of react-query ("4.0.0-alpha.2" and above). To fix that, we need to use an object and assign the list of queries as the value to queries key like below example.
const results = useQueries({queries: [
{
queryKey: ["barQuery", "BAR_TEST_SINGLE"],
queryFn: () => fetchData("BAR_TEST_SINGLE", "models")
},
{
queryKey: ["lineQuery", "LINE_TEST"],
queryFn: () => fetchData("LINE_TEST", "models")
}]});
React-Query V4 Migration docs for reference
I couldn't add a comment as my reputation is currently too low, but I had the same problem as vaibhav-deep, so thought I would put this here even if it's a bit off-topic.
It was fixed by returning whatever my data is in the async/await fetch function.
const queries = useQueries(
queryList.map((query) => {
return {
queryKey: [query],
queryFn: () => fetchData(query),
};
})
);
async function fetchData(query) {
const response = await axios.get(apiCall)...
...
return response.data;
}
Related
I am studying GraphQL/Apollow and React, now I want to fetch data using useQuery(or useLazyQuery). More specifically, I have two queries, query B is dependent on the result of query A, that is, I need to skip query B for some query A results, and not skip for other results. Moreover, I want to have loading spinner while fetching data.
My current solution: useEffect with [] parameters + useLazyQuery (Apollo) to only load data for one time, and at the same time updating loading state.
export const QUERY_A = gql `
// something
`
export const QUERY_B = gql `
// something
`
export default function ExampleModal(props: SomeType) {
const [apiStatus, setApiStatus] = React.useState({
loading: false,
error: false
});
let resultA = '';
const[getA] = useLazyQuery(QUERY_A, {
onCompleted: (data) => { // won't set loading to false, as we need to queryB
resultA = data?.getAquery.status
},
onError: (error) => {
setApiStatus({
loading: false,
error: true
});
},
});
let resultB = '';
const[getB] = useLazyQuery(QUERY_B, {
onCompleted: (data) => {
resultB = data?.getBquery.result;
setApiStatus({
loading: false,
error: false,
});
},
onError: (error) => {
setApiStatus({
loading: false,
error: true
});
},
},
skip: resultA == 'something' // this situation,we skip B
);
React.useEffect( () => {
setApiStatus({ // start loading
loading: true,
error: false
});
// fetch real data from A and B,
}, []); // use [] to just load for one time, when re-render, will not call apis again
if (apiStatus.loading) return <div> loading...<div>
if (apiStatatus.error) return <div> error...<div>
return <div> real result <div>
}
I know there may be a few issues here, my questions is:
does the useEffect will stop immediately after state changes, saying when I
setApiStatus({ // start loading
loading: true,
error: false
});
will the component re-render immediately??
How to store resultA and resultB, to not let them don't fetched again after re-render(I mean when loading stopped and I can get real result); should I do something like
const [apiStatus, setApiStatus] = React.useState({
loading: false,
error: false,
resultA: '',
resultB: '',
});
will this work?
I just start, any suggestions for this kind of problems? any best pratices?
thank you so much!
Your component will always rerender because you have used an object in the state. React doesn't do deep checks on the state.
Either create a new object when doing setState using Object.assign() or separate loading and error.
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(false)
I believe the invocation for GET B should happen inside the callback of GET A so you should only worry about how/when to call GET A.
You can use an empty useEffect and make API call to A conditionally if DATA A is undefined/null.
const [dataA, setDataA] = React.useState(null)
const [dataB, setDataB] = React.useState(null)
useEffect(() => {
!dataA && apiCallForA
}, [])
you can | loadings like
const [callFirst, {loading:loadingFirst}] = useLazyQuery(
GET_QUERY,
{ variables: ... }
);
const [callFirst, {loading:loadingSecond}] = useLazyQuery(
GET_QUERY,
{ variables: ... }
);
return ((loadingFirst | loadingSecond) && loading...)
return <Component/>
or you can call multiple query in one query
export const MULTIPLE_QUERY=gql`
query ($id:Int){
post(id:$id){
id
title
}
comments(id:$id){
id
title
}
}
`
I am using Apollo for graphQL queries in a NextJS project I have the following request. I feel like the solution to this is simple, but the error occurs at the stateData.allProducts section it is saying it is null, but I have set the state in the useEffect and it has data as a dependency in the array, so shouldn't it re-render once data has loaded.
This is all works just fine if I placed these checks below a if statement checking the loading and return ...loading but then I can't use useEffect.
Any help as to what I am doing wrong would be greatly appreciated.
Thanks!
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
// Creating state and setting it from query
const [stateData, setStateData] = useState(null);
const [disableAddToCart, setDisableAddToCart] = useState(false);
useEffect(() => {
setStateData(data);
}, [data]);
//~~~~~~~// <--- RIGHT HERE IS WHERE I GET THE NULL ERROR
const productFound = stateData.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
const currentItem = cartDetails[productFound.id];
useEffect((): void => {
console.log("currentItem", currentItem);
if (currentItem) {
if (currentItem.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}
}, [currentItem]);
As long as your query is loading, or if there is an error, the data variable from useQuery will be null.
Therefore you have to check for the loading to have finished and that no error has occurred. And/or for the data to be defined.
Also, stateData is unnecessary, because data is already a ready-to-use state variable.
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
const [disableAddToCart, setDisableAddToCart] = useState(false);
let productFound;
let currentItem;
if(!loading && !error && data?.allProduct){
productFound = data.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
currentItem = cartDetails[productFound.id];
}
useEffect((): void => {
//since you are using typescript, you can use the optional chaining operator
if (currentItem?.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}, [currentItem]);
I have this data from firestore and I wanted to retrieve it dynamically with a where() but this is the error I'm getting:
TypeError: vaccines is not a function
The user collection:
[![enter image description here][1]][1]
Below are the codes:
const Vaccine = () => {
const [vaccines, setVaccines] = useState([]);
useEffect(() => {
const unsubscribe = firestore
.collection("vaccines")
.onSnapshot((snapshot) => {
const arr = [];
snapshot.forEach((doc) =>
arr.push({
...doc.data(),
id: doc.id,
})
);
setVaccines(arr);
});
return () => {
unsubscribe();
};
}, []);
Preface
As highlighted in the comments on the original question, this query structure is not advised as it requires read access to sensitive user data under /users that includes private medical data.
DO NOT USE THIS CODE IN A PRODUCTION/COMMERICAL ENVIRONMENT. Failure to heed this warning will lead to someone suing you for breaches of privacy regulations.
It is only suitable for a school project (although I would a fail a student for such a security hole) or proof of concept using mocked data. The code included below is provided for education purposes, to solve your specific query and to show strategies of handling dynamic queries in React.
From a performance standpoint, in the worst case scenario (a cache miss), you will be billed one read, for every user with at least one dose of any vaccine, on every refresh, for every viewing user. Even though your code doesn't use the contents of any user document, your code must download all of this data too because the Client SDKs do not support the select() operator.
For better security and performance, perform this logic server-side (e.g. Cloud Function, a script on your own computer, etc) and save the results to a single document that can be reused by all users. This will allow you to properly tighten access to /users. It also significantly simplifies the code you need to display the graphs and live statistics on the client-side.
useEffect
As stated by the React documentation on the Rules of hooks:
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
The documentation further elaborates that React relies on the order in which Hooks are called, which means that you can't have hook definitions behind conditional logic where their order and quantity changes between renders. If your hooks rely on some conditional logic, it must be defined inside of the hook's declaration.
As an example, if you have an effect that relies on other data, with this logic:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
if (userProfile) {
useEffect(() => {
// get user post list and store in userPosts
}, [userProfile]);
}
you need to instead use:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
useEffect(() => {
if (!userProfile) {
// not ready yet/signed out
setUserPosts(null);
return;
}
// get user post list and store in userPosts
}, [userProfile]);
Similarly, for arrays:
someArray && someArray.forEach((entry) => {
useEffect(() => {
// do something with entry to define the effect
}, /* variable change hooks */);
});
should instead be:
useEffect(() => {
if (!someArray) {
// not ready yet
return;
}
const cleanupFunctions = [];
someArray.forEach((entry) => {
// do something with entry to define an effect
cleanupFunctions.push(() => {
// clean up the effect
});
});
// return function to cleanup the effects created here
return () => {
cleanupFunctions.forEach(cleanup => cleanup());
}
}, /* variable change hooks */);
Because this looks a lot like lifecycle management, you are actually better off replacing it with nested components rather than using hooks, like so:
return (
<> // tip: React.Fragment shorthand (used for multiple top-level elements)
{
someArray && someArray
.map(entry => {
return <Entry key={entry.key} data={entry.data} />
})
}
</>
);
Adapting to your code
Note: The code here doesn't use onSnapshot for the statistics because it would cause a rerender every time a new user is added to the database.
const getVaccineStats = (vaccineName) => {
const baseQuery = firestore
.collection("users")
.where("doses.selectedVaccine", "==", vaccine);
const oneDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.get()
.then(querySnapshot => querySnapshot.size);
const twoDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.get()
.then(querySnapshot => querySnapshot.size);
return Promise.all([oneDoseQueryPromise, twoDoseQueryPromise])
.then(([oneDoseCount, twoDoseCount]) => ({ // tip: used "destructuring syntax" instead of `results[0]` and `results[1]`
withOneDose: oneDoseCount,
withTwoDoses: twoDoseCount
}));
};
const Vaccine = () => {
const [vaccines, setVaccines] = useState();
const [vaccineStatsArr, setVaccineStatsArr] = useState([]);
// Purpose: Collect vaccine definitions and store in `vaccines`
useEffect(() => {
return firestore // tip: you can return the unsubscribe function from `onSnapshot` directly
.collection("vaccines")
.onSnapshot({ // tip: using the Observer-like syntax, allows you to handle errors
next: (querySnapshot) => {
const vaccineData = []; // tip: renamed `arr` to indicate what the data contains
querySnapshot.forEach((doc) =>
vaccineData.push({
...doc.data(),
id: doc.id,
});
);
setVaccines(vaccineData);
}),
error: (err) => {
// TODO: Handle database errors (e.g. no permission, no connection)
}
});
}, []);
// Purpose: For each vaccine definition, fetch relevant statistics
// and store in `vaccineStatsArr`
useEffect(() => {
if (!vaccines || vaccines.length === 0) {
return; // no definitions ready, exit early
}
const getVaccineStatsPromises = vaccines
.map(({ vaccine }) => [vaccine, getVaccineStats(vaccine)]);
// tip: used "destructuring syntax" on above line
// (same as `.map(vaccineInfo => [vaccineInfo.vaccine, getVaccineStats(vaccineInfo.vaccine)]);`)
let unsubscribed = false;
Promise.all(getVaccineStatsPromises)
.then(newVaccineStatsArr => {
if (unsubscribed) return; // unsubscribed? do nothing
setVaccineStatsArr(newVaccineStatsArr);
})
.catch(err => {
if (unsubscribed) return; // unsubscribed? do nothing
// TODO: handle errors
});
return () => unsubscribed = true;
}, [vaccines]);
if (!vaccines) // not ready? hide element
return null;
if (vaccines.length === 0) // no vaccines found? show error
return (<span class="error">No vaccines found in database</span>);
if (vaccineStatsArr.length === 0) // no stats yet? show loading message
return (<span>Loading statistics...</span>);
return (<> // tip: React.Fragment shorthand
{
vaccineStatsArr.map(([name, stats]) => {
// this is an example component, find something suitable
// the `key` property is required
return (<BarGraph
key={name}
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>);
});
}
</>);
};
export default Vaccine;
Live Statistics
If you want your graphs to be updated live, you need "zip together" the two snapshot listeners into one, similar to the rxjs combineLatest operator. Here is an example implementation of this:
const onVaccineStatsSnapshot => (vaccine, observerOrSnapshotCallback, errorCallback = undefined) => {
const observer = typeof observerOrCallback === 'function'
? { next: observerOrSnapshotCallback, error: errorCallback }
: observerOrSnapshotCallback;
let latestWithOneDose,
latestWithTwoDoses,
oneDoseReady = false,
twoDosesReady = false;
const fireNext = () => {
// don't actually fire event until both counts have come in
if (oneDoseReady && twoDosesReady) {
observer.next({
withOneDose: latestWithOneDose,
withTwoDoses: latestWithTwoDoses
});
}
};
const fireError = observer.error || (err) => console.error(err);
const oneDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.onSnapshot({
next: (querySnapshot) => {
latestWithOneDose = querySnapshot.size;
oneDoseReady = true;
fireNext();
},
error: fireError
});
const twoDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.onSnapshot({
next: (querySnapshot) => {
latestWithTwoDoses = querySnapshot.size;
twoDosesReady = true;
fireNext();
},
error: fireError
});
return () => {
oneDoseUnsubscribe();
twoDoseUnsubscribe();
};
}
You could rewrite the above function to make use of useState, but this would unnecessarily cause components to rerender when they don't need to.
Usage (direct):
const unsubscribe = onVaccineStatsSnapshot(vaccineName, {
next: (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
},
error: (err) => {
// TODO: error handling
}
);
or
const unsubscribe = onVaccineStatsSnapshot(vaccineName, (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
});
Usage (as a component):
const VaccineStatsGraph = (vaccineName) => {
const [stats, setStats] = useState(null);
useEffect(() => onVaccineStatsSnapshot(vaccineName, {
next: (newStats) => setStats(newStats),
error: (err) => {
// TODO: Handle errors
}
}, [vaccineName]);
if (!stats)
return (<span>Loading graph for {vaccineName}...</span>);
return (
<BarGraph
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>
);
}
vaccines is an array and not a function. You are trying to run a map on vaccines. Try refactoring your code to this:
vaccines &&
vaccines.map((v, index) => {
// ...
})
Also do check: How to call an async function inside a UseEffect() in React?
here is the code, that works for you:
function DatafromFB() {
const[users, setUsers] = useState({});
useEffect(()=>{
const fetchVaccine = async () => {
try {
const docs = await db.collection("vaccines").get();;
docs.forEach((doc) => {
doc.data().vaccineDetails
.forEach(vaccineData=>{
fetchUsers(vaccineData.vaccine)
})
})
} catch (error) {
console.log("error", error);
}
}
const fetchUsers = async (vaccine)=>{
try {
const docs = await db.collection("users")
.where("doses.selectedVaccine", "==", vaccine).get();
docs.forEach(doc=>{
console.log(doc.data())
setUsers(doc.data());
})
}catch(error){
console.log("error", error);
}
}
fetchVaccine();
},[])
return (
<div>
<h1>{users?.doses?.selectedVaccine}</h1>
</div>
)
}
export default DatafromFB
what is ${index.vaccine} I think it must be v.vaccine
also setSize(snap.size); will set set size commonly not vaccine specific
I'm trying to apply the ajax method posted here: https://github.com/redux-observable/redux-observable/blob/master/docs/basics/Epics.md
import { ajax } from 'rxjs/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response))
)
)
);
// later...
dispatch(fetchUser('torvalds'));
When trying this method, I get the message:
TypeError: Object(...)(...).pipe is not a function
So pipe doesn't appear to exist. (It concerns the second pipe after the ajax call).
How do I fix this?
I installed the following dependencies:
"rxjs": "^6.5.2",
"rxjs-compat": "^6.5.2",
Edit:
I changed my code to ajax.get and the calling code:
export const fetchTrendingEpic = action$ => action$.pipe(
ofType(FETCH_TRENDING_REQUEST),
mergeMap(async(action) => {
const res = await fetchPostStats(action.payload);
console.log(res);
res.pipe(
map(response => {
console.log('response', response);
setTrendingPlaces({trendingPlaces: response});
})
)
})
);
The res was properly printed (showing an observable), but now I get an error:
TypeError: Cannot read property 'type' of undefined
This is how I create my store in dev:
const createEnhancer = (epicMiddleware) => {
const middleware = [ epicMiddleware, createLogger() ];
let enhancer;
if (getEnvironment() === 'development') {
enhancer = composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
);
};
export default (initialState) => {
const epicMiddleware = createEpicMiddleware();
const enhancer = createEnhancer(epicMiddleware);
const store = createStore(rootReducer, initialState, enhancer);
epicMiddleware.run(rootEpic);
return store;
}
Edit note:
This is code executed in NodeJS (SSR).
I'm struggling with this, don't really understand how this can be so hard to get working without error.
Can't quite see how the example code will ever work when ajax.getJSON returns a promise, not an Observable...
My service that called the ajax request was marked with async because the previous implementation used Axios before the refactoring.
This was the reason why my console logged a promise as return value of the function.
Removing async returns the Observable as expected.
As #Dez mentioned in the comments, there is also need to add return values.
And last but not least, rxjs ajax does not work in a NodeJS environment.
Therefore, based on this thread:
https://github.com/ReactiveX/rxjs/issues/2099
I have found that someone created the following npm package that I will try out shortly: https://github.com/mcmunder/universal-rxjs-ajax
Axios works fine with rxjs in nodejs, just doing something like this...
const { from } = require('rxjs');
const { map } = require('rxjs/operators');
const axios = require('axios');
const responsePromise = axios.get('https://jsonplaceholder.typicode.com/todos/1');
const response$ = from(responsePromise);
response$
.pipe(
map(response => ({ type: 'RESPONSE_RECEIVED', payload: response.data}))
)
.subscribe(console.log);
this might be kind of long read, I've read and tried so many solutions without success! Essentially what I have is three MySQL tables, one with a list of users, and one with a list of file data. They are paired with a third table, which has a column for user id and a column for file id.
When a user logs into the app, it grabs their ID from Table 1, goes to Table 3, finds all the file IDs that are in the same row as their user ID, and then returns the file information from Table 2. Mostly straight forward, except it's not.
My current code:
componentWillMount() {
this.getClientFiles();
}
Which calls:
getClientFiles() {
let id = this.props.currentUser.user_id;
let file_refs = [];
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.setState({
file_refs
});
this.getFileData();
});
}
My understanding of this is that this.getFileData(); should ONLY run once the axios GET request is successful (because of .then). The file refs are all returned, and the added to an array and put in state for the duration of the client's session.
Then this should run:
getFileData() {
let fileRefs = this.state.file_refs;
let fileData = [];
for (let i = 0; i < fileRefs.length; i++) {
axios
.get("/files/get-file/" + fileRefs[i])
.then(res => {
fileData.push(res.data.response);
this.setState({
client_files: fileData,
returned_data: true
});
})
.catch(err => console.log(err.response.data));
}
}
Here, the function cycles through the fileRefs in state, makes a call for each reference ID, and returns that to fileData and saves it to state.
The problem.... on first page load after a login, the files do not render. If you hit cmd+R to refresh, boom there they are. I understand the chain of promises, and the async nature of JS functions, I understand that componentWillMount() should run prior to the mounting of the component, and that setState should trigger a re-render of a component.
Things I've tried:
1) Adding the following code in after render() prior to return( :
if (this.state.returned_data === false) {
this.getClientFiles();
}
The result is a flickering of renders, 4-5 of them, as the functions run async before the state of returned_data is set to true.
2) Moving the setState({ returned_data: true }) into the getClientFiles() function. This just ends the render early, resulting in no files until the page is refreshed.
3) Swapping out componentWillMount() for componentDidMount().
Clearly, there is a fundamental aspect of the chain of functions and React's built in methods that I'm missing.
Can anybody help?
EDIT #1
The issue seems to be that on first render, let id = this.props.currentUser.user_id; is undefined, so the call in getClientFiles is actually going to /users/get-client-files/undefined
EDIT #2 - Requested by #devserkan
I hope this is what you wanted :)
First load
get-client-files/${id}: Returns an empty array
/get-file/" + fileRefs[i]: Doesn't run
Second load:
get-client-files/${id}: Returns array with 5 items
/get-file/" + fileRefs[i]: Runs appropriately 5 times with the details of each file.
So clearly, the issue is with the fact that get-client-files/${id} isn't getting anything because it doesn't have the ${id} to search from. The ID is passed down via props, but doesn't seem to be available immediately.
EDIT #3
Here is the function that gets the ID, and sets it to state.
getUser = () => {
let localToken = localStorage.getItem("iod_tkn");
axios({
url: "/admins/current",
method: "get",
headers: {
Authorization: localToken
}
})
.then(result => {
this.setState({
isLoggedIn: true,
user: result.data,
user_id: result.data.user_id
});
})
.catch(err => {
this.setState({ isLoggedIn: false });
console.log(err);
});
};
And App.js renders the following:
render() {
const { loading } = this.state;
if (loading) {
return <Spinner />;
}
return (
<AdminProvider>
<FileProvider>
<Provider>
<Appbar isLoggedIn={this.state.isLoggedIn} logout={this.logout} />
<Main
getUser={this.getUser}
isLoggedIn={this.state.isLoggedIn}
currentUser={this.state.user}
/>
<BottomNav />
</Provider>
</FileProvider>
</AdminProvider>
);
}
So with passing this.state.user into Main.js, that component should re-render once the props have been received, right?
Since your user_id is coming from an async job, you should do a conditional rendering. Like:
{ user_id && <ClientDashboard user_id={user_id} ... /> }
Also, you can clean up your code a little bit more maybe :) Here I am mimicking your app.
const userFiles = [
{ file_id: 1, client_name: "foo" },
{ file_id: 2, client_name: "bar" },
{ file_id: 3, client_name: "baz" },
];
const files = [
{ file_id: 1, name: "fizz", size: 10 },
{ file_id: 2, name: "buzz", size: 20 },
{ file_id: 3, name: "fuzz", size: 30 },
];
const fakeRequest = () => new Promise( resolve =>
setTimeout( () => resolve(userFiles), 1000)
);
const fakeRequest2 = id => new Promise(resolve => {
const file = files.find( el => id === el.file_id );
setTimeout(() => resolve(file), 1000)
}
);
class App extends React.Component {
state = {
file_refs: [],
client_files: [],
returned_data: false,
}
componentDidMount() {
this.getClientFiles();
}
getClientFiles() {
fakeRequest()
.then(res => {
const file_refs = res.map( el => el.file_id );
this.setState({
file_refs
});
this.getFileData();
});
}
getFileData() {
const {file_refs: fileRefs} = this.state;
const promiseArray = fileRefs.map( id => fakeRequest2( id ) );
Promise.all( promiseArray )
.then( results => this.setState({
client_files: results,
returned_data: true,
}))
}
render() {
const { file_refs, client_files } = this.state;
return (
<div>
{!!file_refs.length && <p>File_refs: {JSON.stringify(file_refs)}</p>}
{!!client_files.length && <p>Client files: {JSON.stringify(client_files)}</p>}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I don't like for loops :)
The problem is that in componentWillMount() an async call might not retrieve the results on time before the render of the mount phase happens, so you will have unexpected side effects. Most probably the component will render with empty data.
The best place to render data from an async call is componentDidMount().
As a side note, from 16.3 version on, componentWillMount() is considered an unsafe method of the lifecycle, and in future versions will be removed, so you better not use it anymore.
I think there's an issue with your code structuring. setState is an async function which takes a callback as a second parameter. You should take its advantage. You can execute a function after setState is finishing and utilize updated state using the second param callback (updater function) like:
this.setState({
file_refs
}, () => {
this.getFileData();
});
EDITED Second option you shouldn't setState file_refs unless you're using it in your render method.
Try this:
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.getFileData(file_refs);
});
getFileData(file_refs) {
let fileRefs = file_refs;
let fileData = [];
// rest of your code
}
Let me know if the issue still persists. Happy to help