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
}
}
`
Related
I am having issue with useTransition() that it is being set to true but actually never changes back to false.
I am trying to delete record from MongoDB and once it is finished I would like to refresh React Server Component as explained here:
https://beta.nextjs.org/docs/data-fetching/mutating
Issue is that in this case server component won't get refreshed and Button is stucked with loading text.
'use client'
const DeleteButton = ({ details }) => {
const [isPending, startTransition] = useTransition();
const router = useRouter();
const handleDelete = async () => {
await fetch('/api/clients', { method: 'DELETE', body: details._id });
startTransition(() => {
console.log('tran started', isPending);
router.refresh();
});
}
useEffect(() => {
// at load isPending = false
// after start tranisition it is set to false
// but it never returns back to false
console.log('is pending ? ', isPending);
}, [isPending]);
return <Button onClick={() => handleDelete()}>{ isPending ? 'Loading' : 'Delete' }</Button>
}
This is BE code at /api/clients
import type { NextApiRequest, NextApiResponse } from 'next';
import ClientsCollection from '../../db/collections/clients';
import Client from '../../helpers/interfaces/client';
type Data = {
name: string;
};
const clientsCollection = new ClientsCollection();
export default function handler(req: NextApiRequest, res: NextApiResponse<any>) {
switch (req.method) {
case 'DELETE': {
const result = clientsCollection.deleteClientById(req.body);
res.status(200).json(result);
}
default:
res.status(403);
}
}
Closing this.
I misunderstood why error did happen, I was basically having useEffect to react on changes from Redux which for some reason blocked setting isPending back to to false.
I did debug it the way that I've commented out current table that was displaying all records and created simple html table without any functionality which helped me identify which code was wrong.
Hopefully a simply one.
I make an API call in my component which brings down some account information such as AccountUid, Category etc, i use state to set these.
useEffect(() => {
fetch(feed_url, {
headers: {
//Headers for avoiding CORS Error and Auth Token in a secure payload
"Access-Control-Allow-Origin": "*",
Authorization: process.env.REACT_APP_AUTH_TOKEN,
},
})
//Return JSON if the Response is recieved
.then((response) => {
if (response.ok) {
return response.json();
}
throw response;
})
//Set the Account Name state to the JSON data recieved
.then((accountDetails) => {
setAccountDetails(accountDetails);
console.log(accountDetails.accounts[0].accountUid);
console.log(accountDetails.accounts[0].defaultCategory);
})
//Log and Error Message if there is an issue in the Request
.catch((error) => {
console.error("Error fetching Transaction data: ", error);
});
}, [feed_url]);
This Works perfectly well and it Logs the correct values in my .then when testing it.
The issue however is that i want to pass these down as props. But i get an error that they are being returned as null (My default state).. i presume as they're jumping ahead.
<div className="App">
<GetAccountName
accountUID={accountDetails.accounts[0].accountUID}
defCategory={accountDetails.accounts[0].defaultCategory}
/>
</div>
How do i pass the the 2 details im logging as props?? I've tried setting default state to "" instead of null and just get that it is undefined.
If you dont want to use conditional render in your child component, so you should try optional chaining
<GetAccountName
accountUID={accountDetails?.accounts?.[0]?.accountUID}
defCategory={accountDetails?.accounts?.[0]?.defaultCategory}
/>
Since fetching is asyncronous, the most common way is to show some loading indicator (like a spinner) & once the data come in, show the component instead.
If you don't need an indicator, you might just return null.
The general idea is to manipulate some intermediary states (e.g. data, isError) based on the promise state.
Check out react-query library example or a lighter abstraction like useFetch hook to see how they manage it.
Here's a sample implementation of useFetch taken from this article:
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
const [abort, setAbort] = React.useState(() => {});
React.useEffect(() => {
const fetchData = async () => {
try {
const abortController = new AbortController();
const signal = abortController.signal;
setAbort(abortController.abort);
const res = await fetch(url, {...options, signal});
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
return () => {
abort();
}
}, []);
return { response, error, abort };
};
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;
}
I have two components which i am working with. In the first component, i made a custom useEffect hook that retrieves data from my server. Please see the code below:
Code snippet One
import {useState, useCallback} from 'react';
import {stageQuizApi} from '../api/quiz';
import {QuestionService} from "../services/IdDbServices/question_service";
const usePostData = ({url, payload, config}) => {
const [res, setRes] = useState({data: null, error: null, isLoading: false});
const callAPI = useCallback(() => {
setRes(prevState => ({...prevState, isLoading: true}));
stageQuizApi.patch(url, payload, config).then( res => {
setRes({data: res.data, isLoading: false, error: null});
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
}).catch((error) => {
console.log(error)
if (error.response) {
const errorJson = error.response.data
setRes({data: null, isLoading: false, error: errorJson.message});
} else if (error.request) {
setRes({data: null, isLoading: false, eror: error.request});
} else {
setRes({data: null, isLoading: false, error: error.message});
}
})
}, [url, config, payload])
return [res, callAPI];
}
export default usePostData;
The above module has two purpose. It first makes an axios request to my endpoint and secondly makes a database insertion to browser IndexDb (similar to localstorage but with sql approach) ( like inserting data into the database using the response that was gotten from the first request. so typically i have a promise in the outer .then block. This part:
Code snippet Two
const questionInDb = {};
const {NoTimePerQuestion,anwser, question, playerDetails, option} = res.data.data;
const {playerid,anwserRatio, name} = playerDetails
questionInDb.timePerQuestion = NoTimePerQuestion;
questionInDb.anwserRatio = anwserRatio;
questionInDb.options = option;
questionInDb.answer = anwser;
questionInDb.playerId = playerid;
questionInDb.name = name;
questionInDb.question = question;
const Service = new QuestionService();
Service.addStudent(questionInDb).
then(response=>console.log(response))
.catch(err=>console.log(err));
Here is the problem, I am trying to maintain state as i want the result of this module to be shared in another route and i don't want to hit the server again hence i inserted the result into indexDb browser storage. Here is the code that executes the above module:
Code snippet Three
const displaySingleQuestion = ()=>{
OnUserGetQuestion();
history.push('/player/question');
}
The above method is called from my first route /question and it is expected to redirect user to the /player/question when the displaySingleQuestion is called.
On the new route /player/question i then want to fetch the data from IndexDb and update the state of that component using the useEffect code below:
Code snippet Four
useEffect(()=>{
const getAllUserFromIndexDb = async()=>{
try{
const result = await new Promise((resolve, reject) => {
service.getStudents().then(res=>resolve(res)).catch(err=>reject(err))
});
console.log('it did not get to the point i was expecting',result)
if(result[0]){
console.log('it got to the point i was expecting')
const singleQuestion = result[0];
const questionPage = playerQuestionToDisplay;
questionPage.name = singleQuestion.name;
questionPage.anwserRatio = singleQuestion.anwserRatio;
questionPage.answer = singleQuestion.answer;
questionPage.options = singleQuestion.options;
questionPage.playerId = singleQuestion.playerId;
questionPage.question = singleQuestion.question;
questionPage.timePerQuestion = singleQuestion.timePerQuestion;
return setplayerQuestionToDisplay({playerQuestionToDisplay:questionPage})
}
}
catch(error){
console.log(error)
}
}
getAllUserFromIndexDb();
return function cleanup() {
setplayerQuestionToDisplay({playerQuestionToDisplay:{}})
}
},[history.location.pathname]);
The problem is that only one Button click (Code snippet three)(displaySingleQuestion()) triggers the whole functionality and redirect to the /player/question page but in this new route the state is not been set until a page reload as occurred, i tried debugging the problem and i found out that when the button is clicked i found out that Code snippet two is executed last hence when Code snippet Four ran it was in promise and until a page reloads occurs the state of the component is undefined
Thanks for reading, Please i would appreciate any help in resolving this issue.
I'm using React hooks both to fetch GraphQL data with react-apollo and to store local state:
const [userData, setUserData] = useState({})
const { loading, error, data } = useQuery(USER_QUERY)
However, I'm wondering how to store data to userData. Is this how it's supposed to work:
useEffect(() => {
setUserData(data)
}, [Object.entries(data).length])
Looks like what you have probably works. There is also a onCompleted option available in the options parameter. it takes a callback of type:
(data: TData | {}) => void
so this is another way of doing it:
const { loading, error, data } = useQuery(USER_QUERY, {onCompleted: setUserData})
What are you trying to do with the returned data that you are unable to accomplish by simply using it as destructured from the query hook? In most use cases it can be used immediately, as it will update itself when refetched.
If it is necessary (and it could be), as the other answer says, the useEffect hook you posted should work, but I would replace the dependency with simply data, to prevent an edge case where the response has an equal length consisting of different data and does not update:
useEffect(() => {
setUserData(data)
}, [data])
I think something like this would work - you will need to create the initial state with useState, could be empty array and then onComplete in the useQuery would setTranscationsData... it is triggered every render when state or props change. Could of course add an inital state inside useState which insn't an empty array.
const [transactionsData, setTransactionsData] = React.useState([]);
const { error, data } = useQuery(GET_TRANSACTIONS, {
onCompleted: () => {
setTransactionsData(data.transactions);
},
});
another example
const [getLegalStatement] = useLazyQuery(GET_LEGAL_STATEMENT, {
fetchPolicy: 'network-only',
onCompleted: (data) => {
setTempLegalStatement(data.getLegalStatement);
},
onError: () => {
setTempLegalStatement({
consentedLegalStatementHash: '',
consentedSuppliersHash: '',
statement: '',
suppliersModal: '',
});
setTimeout(() => {
setRefetchNeeded(true);
}, 10000);
},
});
Use onSuccess
const [userData, setUserData] = useState({})
const { data, isLoading, error } = useQuery('QueryKey', QueryFunction, { onSuccess: setUserData })
This onSuccess callback function will fire setUserData(data) for you automatically any time the query successfully fetches new data.
To elaborate above, you can't use onSuccess/onSettled because those will not rerun if the data is cached, so if you leave the component and come back before the query expires your data won't get set.