I have a situation where I need to fetch e.g. all articles posted by a user when a component is mounted. To get a user's articles I am using the following query:
const GET_USER_ARTICLES = gql`
query getUserArticles($id: ID, $numArticles: Int!, $cursor: String) {
user(id: $id) {
id
articles(first: $numArticles, after: $cursor, orderBy: "-created", state: "enabled") #connection(key: "userArticles") {
edges {
node {
name
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
`;
If there is a next page I want to keep fetching more articles until I have ALL of them. Up until now I haven't had the need to do anything like this (normally I have a button the user can click "Load more" to fetch more articles for example, but now need to fetch everything without a user interacting with anything), so I'm not sure what the best way to go about this is.
An example of the query in React:
const PAGE_SIZE = 10;
const { data, loading, fetchMore } = useQuery<UserArticlesData, UserArticlesVariables>(
GET_USER_ARTICLES,
{ variables: { id: userId, numArticles: PAGE_SIZE, cursor: null } },
);
I am a little lost how I can use the fetchMore to keep fetching until there aren't any more pages left, while also showing a loading state to the user. I'm also not sure this is the best way to go about this in the first place, so any suggestions are more than welcome!
If the API does not limit the page size, you could just provide an arbitrarily large number as the page size to get the remaining results. Assuming the page size can only be so big, though, you can do something like this:
const { data, loading, fetchMore } = useQuery(GET_USER_ARTICLES, {
variables: { id: userId, numArticles: PAGE_SIZE, cursor: null },
notifyOnNetworkStatusChange: true,
})
const fetchRest = async () => {
const { user: { articles: { pageInfo } } } = data
const updateQuery = (prev, { fetchMoreResult }) => {
// Merge the fetchMoreResult and return the combined result
}
let hasNextPage = pageInfo.hasNextPage
let cursor = pageInfo. endCursor
while (hasNextPage) {
const { data } = await fetchMore({
variables: { id: userId, numArticles: PAGE_SIZE, cursor },
updateQuery,
})
const { user: { articles: { pageInfo } } } = data
hasNextPage = pageInfo.hasNextPage
cursor = pageInfo. endCursor
}
}
By setting notifyOnNetworkStatusChange to true, loading will be updated whenever fetchMore is doing any fetching. Then we just loop until hasNextPage is called. fetchMore returns a Promise that resolves to the query result, so we can use the query response outside the updateQuery function.
Note that this is a rough example -- you might actually want to keep track of loading state yourself, for example. If your API has rate limiting, your logic should account for that as well. However hopefully this gives you a good starting point.
Edit:
If you need to get all the articles initially, I wouldn't use useQuery and fetchMore at all. The easiest workaround would be to manage the data and loading state yourself and utilize client.query instead.
const client = useApolloClient()
const [data, setData] = useState()
const [loading, setLoading] = useState(true)
const fetchAll = async () => {
let hasNextPage = true
let cursor = null
let allResults = null
while (hasNextPage) {
const { data } = await client.query(GET_USER_ARTICLES, {
variables: { id: userId, numArticles: PAGE_SIZE, cursor },
})
// merge data with allResults
hasNextPage = pageInfo.hasNextPage
cursor = pageInfo. endCursor
}
setLoading(false)
setData(allResults)
}
useEffect(() => {
fetchAll()
}, [])
Related
I have a hook (useDashboardData) that calls another hook (useItems) that's just a wrapper for two Apollo client queries.
inside the first hook useDashboardData, i'm also calling another hook useOtherItems that also calls another Apollo client query.
export const useDashboardData = () => {
const { item, isItemsLoading } = useItems();
const { list, isOtherItemsLoading } = useOtherItems();
const dashboardData = {
items: {
itemsLoading: isItemsLoading,
itemsData: item,
},
otherItems: {
otherItemsLoading: isOtherItemsLoading,
otherItemsData: list,
},
};
return {
dashboardProps: {
dashboardData: dashboardData,
},
};
};
useItems.tsx
export const useItems = () => {
const { user } = useAuthorization();
const {
data: itemData,
loading: items Loading,
} = useCustomApolloGetItemsQuery({
skip: !user.id,
variables: { user.id },
});
const {
data: moreItemData,
loading: moreItemsLoading,
} = useAnotherApolloGetItemsQuery({
skip: !user.id,
variables: { user.id },
});
const combinedItems = combineItemData(itemData, moreItemData);
return { combinedItems, ItemsLoading };
useOtherItems.tsx
export const useOtherItems = () => {
const { user } = useAuthorization();
const { data: list, loading: isOtherItemsLoading } = useGetInvoiceListQuery({
skip: !user.id,
variables: {
userId: user.id,
},
});
return { list, isOtherItemsLoading };
For some reason, anytime I introduce the second hook, the previous requests get canceled. which one is arbitrary but it's consistently canceled.
I'm pretty sure it's due to the first hook request resolving earlier and causing a re-render before the request in the second hook is resolved.
I need to figure out the right pattern to deal with this.
**note I have made sure the Apollo Client is only instantiated once so it's not that.
const getQueryService = () => {
return {
login: async (id): Promise<AuthLoginGoogleResponse> => {
try {
const result = await authApi.loginGooglePost({
idToken: {
id_token: id,
},
});
return result;
} catch (error) {
console.error("Google Login Fail", error);
}
},
};
};
// Mutation is only for updating and creating and deleting
const getMutationService = () => {
return {};
};
const useGoogleLogin = () => {
const queryClient = useQueryClient();
const queryService = getQueryService();
// const { data, isLoading } = useQuery('auth', queryService.login)
const mutationService = getMutationService();
const fetchLoginData = async (
tokenId
): Promise<AuthLoginGoogleResponse | void> => {
return await queryClient.prefetchQuery("auth", async() => {
return await queryService.login(tokenId);
});
};
return fetchLoginData;
};
I am sending token.Id to API using Post request and I am calling it from component however when I run debugger, preFetchquery is not returning the value retuned from result in getqueryservice function.
Is there a reason why preFetchQuery is not returning the return value from getQueryService.login?
because that's what prefetching does. According to the docs (emphasis mine):
prefetchQuery is an asynchronous method that can be used to prefetch a query before it is needed or rendered with useQuery and friends. The method works the same as fetchQuery except that it will not throw or return any data.
So prefetchQuery just puts data in the cache so that it can be picked up later by useQuery, hence the name: pre-fetch.
If you wan to get data returned, you can use queryClient.fetchQuery instead - but you'd also need to handle errors in case the fetch fails.
To be honest, I'm not sure why you are trying to achieve though. Judging from the code, it looks like you're trying to execute a query when the user wants to login. Please keep in mind that this is not what queries are for. Logging someone in is a prime example for a mutation.
I use react-apollo useLazyQuery hook. My goal to use it in fetch more button click. However, when I try to use it the fetchMore function is undefined.
Here the call to it:
const [getMyData,{ loading, data, fetchMore }] = useLazyQuery(
myQuery,
{
notifyOnNetworkStatusChange: true,
fetchPolicy: 'network-only',
variables: {}
})
Even if I run lazy on component ready, fetchMoreStill undefined.
useEffect(()=>{
getMyData()
},[])
The question is why it is undefined?
"apollo-client": "^2.6.4", "react-apollo": "^3.1.3",
You need to run the loadItem first. My case also loads on demand because I'm using SSR.
Here is my work around, check the fetchMore is undefined to make the decision
function NewItemListingPagination({
initialItems,
initialPage,
initialLimit,
initialTotal,
className
}: ComponentPropTypes) {
const { t } = useTranslation(['homepage', 'common'])
const page = useRef<number>(initialPage)
const [loadItem, { loading, data, fetchMore }] = useLazyQuery(FEED_NEW_ITEM)
async function loadMore(): Promise<void> {
const newPage = page.current + 1
const offset = (newPage - 1) * initialLimit
page.current = newPage
if (!fetchMore) {
loadItem({
variables: {
limit: 12,
offset
}
})
} else {
fetchMore({
query: FEED_NEW_ITEM,
variables: {
limit: 12,
offset
},
updateQuery: (previousResult, {fetchMoreResult}) => {
console.log('dddd', previousResult, fetchMoreResult)
return previousResult
}
})
}
}
const items = !data ? initialItems : initialItems.concat(data.feed.items)
return <div></div>
NOTE: this code may not optimized, I just want to show the flow of fetchMore.
I am also new to react but as far as I know you have to use refetch and than give it a name like this:
const [getMyData,{ loading, data, refetch: fetchMore }] = useLazyQuery...
you can do the same thing with data and loading.
const [getMyData,{ loading: myLoading, data: myData, refetch: fetchMore }] = useLazyQuery...
I have a query on my App.js:
import { gql } from 'apollo-boost';
const ALL_ITEMS_QUERY = gql`
query ALL_ITEMS_QUERY {
challenges {
id
title
}
goals {
id
title
completed
createdAt
updatedAt
steps {
id
completed
title
}
}
}
`;
And i am looking to write a simple deleteGoal mutation:
const DeleteWrapper = (props) => {
const [deleteGoal, { data }] = useMutation(DELETE_ITEM_MUTATION, {
update(cache, payload) {
const data = cache.readQuery({ query: ALL_ITEMS_QUERY });
data.goals = data.goals.filter(
(goal) => goal.id !== payload.data.deleteGoal.id
);
cache.writeQuery({ query: ALL_ITEMS_QUERY, data });
},
});
}
The function returns the modified array correctly, but the item never disappears from the frontend list. I have a hunch that this is related to querying multiple categories at once (goals and challenges, rather than goals only).
Even though the cache seems to be modified correclty, why does the item never disappear, why does the re-render never happen?
After some trial and error I found out that I have to lay out the exact data object to the writeQuery function. I don't really understand why, since the challenges object was left untouched after the query. I have not been able to make this work otherwise.
const DeleteWrapper = (props) => {
const [deleteGoal] = useMutation(DELETE_ITEM_MUTATION, {
update(cache, { data: { deleteGoal} }) {
const { goals, challenges } = cache.readQuery({ query: ALL_ITEMS_QUERY });
const newArr = goals.filter((goal) => goal.id !== deleteGoal.id);
cache.writeQuery({
query: ALL_ITEMS_QUERY,
data: { challenges, goals: newArr },
});
},
});
}
I'm building a discord/slack clone. I have Channels, Messages and users.
As soon as my Chat component loads, channels get fetched with useQuery hook from Apollo.
By default when a users comes at the Chat component, he needs to click on a specific channels to see the info about the channel and also the messages.
In the smaller Channel.js component I write the channelid of the clicked Channel to the apollo-cache. This works perfect, I use the useQuery hooks #client in the Messages.js component to fetch the channelid from the cache and it's working perfect.
The problem shows up when I use the useLazyQuery hook for fetching the messages for a specific channel (the channel the user clicks on).
It causes a infinite re-render loop in React causing the app to crash.
I've tried working with the normal useQuery hook with the skip option. I then call the refetch() function when I need it. This 'works' in the sense of it not giving me infinite loop.
But then the console.log() give me this error: [GraphQL error]: Message: Variable "$channelid" of required type "String!" was not provided. Path: undefined. This is very weird because my schema and variables are correct ??
The useLazyQuery does give me infinite loop as said before.
I'm really struggling with the conditionality of apollo/react hooks...
/// Channel.js component ///
const Channel = ({ id, channelName, channelDescription, authorName }) => {
const chatContext = useContext(ChatContext);
const client = useApolloClient();
const { fetchChannelInfo, setCurrentChannel } = chatContext;
const selectChannel = (e, id) => {
fetchChannelInfo(true);
const currentChannel = {
channelid: id,
channelName,
channelDescription,
authorName
};
setCurrentChannel(currentChannel);
client.writeData({
data: {
channelid: id
}
});
// console.log(currentChannel);
};
return (
<ChannelNameAndLogo onClick={e => selectChannel(e, id)}>
<ChannelLogo className='fab fa-slack-hash' />
<ChannelName>{channelName}</ChannelName>
</ChannelNameAndLogo>
);
};
export default Channel;
/// Messages.js component ///
const FETCH_CHANNELID = gql`
{
channelid #client
}
`;
const Messages = () => {
const [messageContent, setMessageContent] = useState('');
const chatContext = useContext(ChatContext);
const { currentChannel } = chatContext;
// const { data, loading, refetch } = useQuery(FETCH_MESSAGES, {
// skip: true
// });
const { data: channelidData, loading: channelidLoading } = useQuery(
FETCH_CHANNELID
);
const [fetchMessages, { data, called, loading, error }] = useLazyQuery(
FETCH_MESSAGES
);
//// useMutation is working
const [
createMessage,
{ data: MessageData, loading: MessageLoading }
] = useMutation(CREATE_MESSAGE);
if (channelidLoading && !channelidData) {
console.log('loading');
setInterval(() => {
console.log('loading ...');
}, 1000);
} else if (!channelidLoading && channelidData) {
console.log('not loading anymore...');
console.log(channelidData.channelid);
fetchMessages({ variables: { channelid: channelidData.channelid } });
console.log(data);
}
I expect to have messages in data from the useLazyQuery ...But instead get this in the console.log():
react-dom.development.js:16408 Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.
You could use the called variable return by useLazyQuery.
!called && fetchMessages({ variables: { channelid: channelidData.channelid } });
You call fetchMessages on every render.
Try to put fetchMessages in a useEffect :
useEffect(() => {
if (!channelidLoading && channelidData) {
fetchMessages();
}
}, [channelidLoading, channelidData]);
Like that the fetchMessages function only calls when
channelidLoading or channelidData is changing.
You could also look at doing the following:
import debounce from 'lodash.debounce';
...
const [fetchMessages, { data, called, loading, error }] = useLazyQuery(
FETCH_MESSAGES
);
const findMessageButChill = debounce(fetchMessages, 350);
...
} else if (!channelidLoading && channelidData) {
findMessageButChill({
variables: { channelid: channelidData.channelid },
});
}