GraphQL with Apollo - how to use fetchMore - reactjs

I have a problem with understanding Apollo fetchMore method. I'm new to this tool and example in doc is too much complicated for me. I was searching on the internet but I can't find more (current) examples. Can you help me write a most simple example of infinite loading data? I have this code:
const productInfo = gql`
query {
allProducts {
id
name
price
category {
name
}
}
}`
const ProductsListData = graphql(productInfo)(ProductsList)
and I want to fetch 5 products and 5 more after every click on 'Load More' button. I understand the idea of this - cursor e.t.c but I don't know how to implement this. (I'm using React)

For infinite loading, you would use a cursor. Referencing the example on the Apollo documentation, http://dev.apollodata.com/react/pagination.html#cursor-pages
Tailoring this to your schema you've provided, it would look something like this (Untested)
const ProductsListData = graphql(productInfo, {
props({ data: { loading, cursor, allProducts, fetchMore } }) {
return {
loading,
allProducts,
loadMoreEntries: () => {
return fetchMore({
variables: {
cursor: cursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const previousEntry = previousResult.entry;
const newProducts = fetchMoreResult.allProducts;
return {
cursor: fetchMoreResult.cursor,
entry: {
allProducts: [...previousEntry.entry.allProducts, ...newProducts],
},
};
},
});
},
};
},
})(ProductsList);
You'll likely have to play around with the pathing of the objects as this should look similar to your schema. But for the most part this is what your infinite scrolling pagination implementation should look like.

Check out this tutorial by graphql.college https://www.graphql.college/building-a-github-client-with-react-apollo/
Also, you can take a look at my implementation on Github. Specifically, check ./src/views/profile on the query-mutation-with-hoc branch

Related

How do I invalidate queries and fetch latest data in TRPC?

I am simply trying to get latest data from the server after performing a mutation. My code looks something like this:
const utils = trpc.useContext()
const markAsUnreadMutation = trpc.useMutation(['update-mark-as-unread'], {
onSuccess() {
utils.invalidateQueries() //THIS IS NOT WORKING!
},
onError(data) {
toast({
type: 'error',
message: data.message,
})
},
})
function markAsUnread(isUnread: boolean) {
markAsUnreadMutation.mutate({
id: parseInt(channel.id),
markAsUnread: isUnread,
})
}
Invalidating a single query
You can invalidate a query relating to a single procedure and even filter based on the input passed to it to prevent unnecessary calls to the back end.
import { trpc } from '../utils/trpc';
function MyComponent() {
const utils = trpc.useContext();
const mutation = trpc.post.edit.useMutation({
onSuccess(input) {
utils.post.all.invalidate();
utils.post.byId.invalidate({ id: input.id }); // Will not invalidate queries for other id's 👍
},
});
// [...]
}
If you are using TRPC v10, you can do something like this:
utils.your.route.invalidate()

How do you access query arguments in getSelectors() when using createEntityAdapter with RTK Query

I've been following along the REDUX essentials guide and I'm at part 8, combining RTK Query with the createEntityAdapter. I'm using the guide to implement it in a personal project where my getUni endpoint has an argument named country, as you can see from the code snippet below.
I'm wondering is there anyway to access the country argument value from the state in universityAdaptor.getSelector(state => ) at the bottom of the snippet, as the query key name keeps changing.
import {
createEntityAdapter,
createSelector,
nanoid
} from "#reduxjs/toolkit";
import {
apiSlice
} from "../api/apiSlice";
const universityAdapter = createEntityAdapter({})
const initialState = universityAdapter.getInitialState();
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUni: builder.query({
query: country => ({
url: `http://universities.hipolabs.com/search?country=${country}`,
}),
transformResponse: responseData => {
let resConvert = responseData.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(each => {
return { ...each,
id: nanoid()
}
});
return universityAdapter.setAll(initialState, resConvert)
}
})
})
});
export const {
useGetUniQuery
} = extendedApiSlice;
export const {
selectAll: getAllUniversity
} = universityAdapter.getSelectors(state => {
return Object.keys({ ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }).length === 0
? initialState : { ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }
})
UPDATE: I got it working with a turnery operator due to the multiple redux Actions created when RTK Query handles fetching. Wondering if this is best practice as I still haven't figured out how to access the country argument.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
return !Object.values(state.api.queries)[0]
? initialState : Object.values(state.api.queries)[0].status !== 'fulfilled'
? initialState : Object.values(state.api.queries)[0].data
})
I wrote that "Essentials" tutorial :)
I'm actually a bit confused what your question is - can you clarify what specifically you're trying to do?
That said, I'll try to offer some hopefully relevant info.
First, you don't need to manually call someEndpoint.select() most of the time - instead, call const { data } = useGetThingQuery("someArg"), and RTKQ will fetch and return it. You only need to call someEndpoint.select() if you're manually constructing a selector for use elsewhere.
Second, if you are manually trying to construct a selector, keep in mind that the point of someEndpoint.select() is to construct "a selector that gives you back the entire cache entry for that cache key". What you usually want from that cache entry is just the received value, which is stored as cacheEntry.data, and in this case that will contain the normalized { ids : [], entities: {} } lookup table you returned from transformResponse().
Notionally, you might be able to do something like this:
const selectNormalizedPokemonData = someApi.endpoints.getAllPokemon.select();
// These selectors expect the entity state as an arg,
// not the entire Redux root state:
// https://redux-toolkit.js.org/api/createEntityAdapter#selector-functions
const localizedPokemonSelectors = pokemonAdapter.getSelectors();
const selectPokemonEntryById = createSelector(
selectNormalizedPokemonData ,
(state, pokemonId) => pokemonId,
(pokemonData, pokemonId) => {
return localizedPokemonSelectors.selectById(pokemonData, pokemonId);
}
)
Some more info that may help see what's happening with the code in the Essentials tutorial, background - getLists endpoint takes 1 parameter, select in the service:
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId) : [];
};
And my selector in the slice:
export const selectAllLists = createSelector(getListsResult, (listsResult) => {
console.log('inside of selectAllLists selector = ', listsResult);
return listsResult.data;
// return useSelector(listsResult) ?? [];
});
Now this console logs listsResult as ƒ memoized() { function! Not something that can have .data property as tutorial suggests. Additionally return useSelector(listsResult) - makes it work, by executing the memoized function.
This is how far I got, but from what I understand, the code in the Essentials tutorial does not work as it is...
However going here https://codesandbox.io/s/distracted-chandrasekhar-r4mcn1?file=/src/features/users/usersSlice.js and adding same console log:
const selectUsersData = createSelector(selectUsersResult, (usersResult) => {
console.log("usersResult", usersResult);
return usersResult.data;
});
Shows it is not returning a memorised function, but an object with data on it instead.
Wonder if the difference happening because I have a parameter on my endpoint...
select returns a memoized curry function. Thus, call it with first with corresponding arg aka tribeId in your case and then with state. This will give you the result object back for corresponding chained selectors.
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId)(state) : [];
};
The intention of the getUni endpoint was to produce an array of university data. To implement the .getSelector function to retrieve that array, I looped over all query values, searching for a getUni query and ensuring they were fulfilled. The bottom turnery operator confirms the getUni endpoint was fired at least once otherwise, it returns the initialState value.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
let newObj = {};
for (const value of Object.values(state.api.queries)) {
if (value?.endpointName === 'getUni' && value?.status === 'fulfilled') {
newObj = value.data;
}
}
return !Object.values(newObj)[0] ? initialState : newObj;
})

FetchMore : Request executed twice every time

I am trying to implement pagination in a comment section.
I have normal visual behavior on the website. When I click the get more button, 10 new comments are added.
My problem is the request is executed twice every time. I have no idea why. The first time, it is executed with a cursor value, the second time without it. It seems that the useQuery hook is executed after each fetchMore.
Any help would be appreciated. Thanks!
component :
export default ({ event }) => {
const { data: moreCommentsData, fetchMore } = useQuery(getMoreCommentsQuery, {
variables: {
id: event.id,
},
fetchPolicy: "cache-and-network",
});
const getMoreComments = () => {
const cursor =
moreCommentsData.event.comments[
moreCommentsData.event.comments.length - 1
];
fetchMore({
variables: {
id: event.id,
cursor: cursor.id,
},
updateQuery: (prev, { fetchMoreResult, ...rest }) => {
return {
...fetchMoreResult,
event: {
...fetchMoreResult.event,
comments: [
...prev.event.comments,
...fetchMoreResult.event.comments,
],
commentCount: fetchMoreResult.event.commentCount,
},
};
},
});
};
return (
<Container>
{moreCommentsData &&
moreCommentsData.event &&
moreCommentsData.event.comments
? moreCommentsData.event.comments.map((c) => c.text + " ")
: ""}
<Button content="Load More" basic onClick={() => getMoreComments()} />
</Container>
);
};
query :
const getMoreCommentsQuery = gql`
query($id: ID, $cursor: ID) {
event(id: $id) {
id
comments(cursor: $cursor) {
id
text
author {
id
displayName
photoURL
}
}
}
}
`;
Adding
nextFetchPolicy: "cache-first"
to the useQuery hook prevents making a server call when the component re renders.
This solved my problem.
Could it be that the second request you are seeing is just because of refetchOnWindowFocus, because that happens a lot...
I had a similar problem and I solved it by changing the fetchPolicy to network-only.
Please note, if you are hitting this issue with #apollo/client v3.5.x there was a bug related to this which was fixed from v3.6.0+. That commit solved the duplicated execution issue in my case.
It only makes one request if you are worried about that but the react component refreshes but not rerender each time an item in the useQuery hook changes.
Take for instance it would refresh the component when loading changes, making it seem like it sends multiple requests.

ApolloClient v3 fetchMore with nested query results

I'm using ApolloClient 3 the GitHub GraphQL API to retrieve all releases from a repo.
This is what the query looks like:
query ($owner: String!, $name: String!, $first: Int, $after: String, $before: String) {
repository(owner: $owner, name: $name) {
id
releases(orderBy: {field: CREATED_AT, direction: DESC}, first: $first, after: $after, before: $before) {
nodes {
name
publishedAt
resourcePath
tagName
url
id
isPrerelease
description
descriptionHTML
}
totalCount
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
}
This is what the result payload looks like:
This returns me the first x entries (nodes). So far, all good.
I need to implement pagination and I make use of the fetchMore function provided by ApolloClient useQuery. Calling fetchMore fetches the next x entries successfully but these are not displayed in my component list.
According to the ApolloClient Pagination documentation, it seems necessary to handle the merging of the fetchMore results with the ApolloClient caching mechanism. The documentation is understandable for simple situations but I am struggling to implement a solution for the situation where the actual array of results that needs to be merged togeher is deeply nested in the query result (repository -> releases -> nodes).
This is my implementation of the InMemoryCache options merge:
const inMemoryCacheOptions = {
addTypename: true,
typePolicies: {
ReleaseConnection: {
fields: {
nodes: {
merge(existing, incoming, options) {
const previous = existing || []
const results = [...previous, ...incoming]
return results
}
}
}
},
}
}
The results array here contains the full list, including the existing entries and the new x entries. This is essentially the correct result. However, my component list which is using the useQuery and fetchMore functionality does not get the new entries after the fetchMore is called.
I have tried various combinations in the inMemoryCacheOptions code above but so far I have been unsuccessful.
To add more context, this is the related component code:
export default function Releases() {
const { loading, error, data, fetchMore } = useQuery(releasesQuery, {
variables: {
owner: "theowner",
name: "myrepo",
first: 15
}
});
if (loading) return null;
if (error) {
console.error(error);
return null;
}
if (data) {
console.log(data?.repository?.releases?.pageInfo?.endCursor);
}
const handleFetchMore = () => {
fetchMore({
variables: {
first: 15,
after: data?.repository?.releases?.pageInfo?.endCursor
}
});
};
return (
<div>
<ul>
{data?.repository?.releases?.nodes?.map(release => (
<li key={release.id}>{release.name}</li>
))}
</ul>
<button onClick={handleFetchMore}>Fetch More</button>
</div>
);
}
After fetchMore the component doesn't rerender with the new data.
If anyone has any other ideas that I could try, I'd be grateful.
I finally managed to solve this. There was no change to the react component code but the InMemoryCacheOptions now looks like this:
const inMemoryCacheOptions = {
addTypename: true,
typePolicies: {
Repository: {
fields: {
releases: {
keyArgs: false,
merge(existing, incoming) {
if (!incoming) return existing;
if (!existing) return incoming;
const { nodes, ...rest } = incoming;
// We only need to merge the nodes array.
// The rest of the fields (pagination) should always be overwritten by incoming
let result = rest;
result.nodes = [...existing.nodes, ...nodes];
return result;
}
}
}
}
}
};
The main change from my original code is that I now define the typePolicy for the releases field of the Repository type. Previously I was trying to get directly to the nodes field of the Release type. Since my Repository type the root of the gql query and used in the component, it now reads the merged results from the cache.
If I specified the typePolicy for Query as mentioned in the docs, I would not be able to specify the merge behaviour for the releases field because it would be one level too deep (i.e. Query -> repository -> releases). This is what lead to my confusion in the beginning.

How to query relational data with GraphQL, Firebase and Gatsby

I'm building a Gatsby.js site.
The site uses the gatsby-source-firestore plugin to connect to the Firestore data source.
My question is this. How can I query relational data? As in, fetch data from two models at once, where modelA[x] = modelB[y]
I don't really understand resolvers. I don't think I have any.
Note, I am not considering graph.cool currently. I'd like to stick with Firebase. I will do the relational data matching in pure JS if I have to (not GraphQL).
Here is what my gatsby-config.js looks like:
{
resolve: 'gatsby-source-firestore',
options: {
credential: require('./firebase-key.json'),
databaseURL: 'https://testblahblah.firebaseio.com',
types: [
{
type: 'Users',
collection: 'users',
map: user => ({
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
ownsHat: user.ownsHat,
hatId: user.hatId
})
},
{
type: 'Hats',
collection: 'hats',
map: hat => ({
hatType: hat.hatType,
hatUserId: hat.hatUserId,
hatId: hat.hatId
})
}
]
}
},
This pulls in two flat data models. I can query like this in-page:
any-page.js
export const query = graphql`
query {
allUsers {
edges {
node {
...UserFragment
}
}
}
}
`
What I'm looking for is a query that lets me write one query inside another i.e. a relational data query within a query.
export const query = graphql`
query {
allUsers {
edges {
node {
...UserFragment {
hats (user.userId == hat.userId){
type
hatId
}
}
}
}
}
}
`
As you can understand, this amounts to: How to run multiple GraphQL queries of relational data.
Given the nature of Firestore's flat JSON, this makes the relational aspect of GraphQL difficult.
I'm really keen to understand this better and would really appreciate being pointed down the right path.
I am really keen on sticking with GraphQL and Firebase.
Thanks!
I'm not sure this works in graphql but in Gatsby you can use gatsby-node to create and alter your nodes and inject hats to each user node. Here's an example code I'm using to add authors to a Post node:
const mapAuthorsToPostNode = (node, getNodes) => {
const author = getPostAuthorNode(node, getNodes);
if (author) node.authors___NODES = [author.id];
};
exports.sourceNodes = ({actions, getNodes, getNode}) => {
const {createNodeField} = actions;
getCollectionNodes('posts', getNodes).forEach(node => {
mapAuthorsToPostNode(node, getNodes);
});
};
This is one way to do it provided the records are not in huge numbers. If they are, you should create a hats page to display user hats where you query just the hats filtered by user id which is received via a paceContext param such as user id.
export const pageQuery = graphql`
query($userId: String) {
hats: allHats(
filter: {
userId: {eq: $userId}
}
) {
edges {
node {
...hatFragment
}
}
}
}
`;

Resources