Query on page not refetching once navigating back to page - graphql

In my nextjs page I have the following hook (generated by using graphql-codegen) that fetches a graphql query.
const { data, error, loading, fetchMore, refetch, variables } = useGetShortlistQuery({
notifyOnNetworkStatusChange: true, // updates loading value
defaultOptions: {
variables: {
offset: undefined,
filterBy: undefined,
sortBy: SortBy.RecentlyAdded,
sortDirection: SortDirection.Desc,
},
},
});
This is the useGetShortlistQuery hook that is generated by graphql-codegen
export function useGetShortlistQuery(
baseOptions?: Apollo.QueryHookOptions<GetShortlistQuery, GetShortlistQueryVariables>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetShortlistQuery, GetShortlistQueryVariables>(GetShortlistDocument, options);
}
my component is wrapped in a HOC to enable Apollo Client
export default withApollo({ ssr: true })(Index);
The withApollo HOC uses #apollo/client and the cache property of the apollo client is as follows.
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
getShortlist: {
keyArgs: [],
merge(existing: PaginatedProperties | undefined, incoming: PaginatedProperties): PaginatedProperties {
return {
...incoming,
properties: [...(existing?.properties || []), ...(incoming?.properties || [])],
};
},
},
},
},
},
}),
The problem I am having is that on this page I update the variables on the useGetShortlistQuery using refetch which, in turn, updates the data.
However, if I navigate to another page, then come back to this page using this component. It doesn't seem to retrigger the graphql query so returns the previous data.

If you are using getStaticProps (or getServerSideProps) with pre rendered pages, it is a known behavior. It is due to optimisation by Next.js not re-rendering components between page navigations, with pages like [id].js.
The trick is to have a key on components that you want to see refreshing. You have multiple ways to do so. Having a different key on components tells React that it should be re-rendering the components, and thus will trigger again the hooks.
Practical example:
export const getStaticProps: GetStaticProps = async ({ params }) => {
const data = getData() //something that fetches your data here
return {
props: {
// some other data you want to return...
// some unique value that will be on each page
key: data.key
},
}
}
const MyPage: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = (props) => {
<div key={props.key} />
}

Related

RTK Query - Infinite Scrolling, retaining existing data

I am attempting to implement infinite scrolling functionality in our current application;
We first fetch the first 5 'Posts' for a page. Upon scrolling to the bottom of the page, we then fetch the next 5 Posts.
This works nicely, however using the same query means that the existing data (the first 5 posts) has been replaced by the new data.
Is it possible to merge the existing data with the new data?
I could merge them in place, for example with something like; const posts = [newPosts, oldPosts] but then we lose the data invalidation provided by RTK Query if the existing data is modified.
What is the recommended approach for this case?
In RTK 1.9 it is now possible to use the merge option to merge newly fetched data with the data that currently lives inside the cache. Make sure you use the option together with serializeQueryArgs or forceRefetch to keep a cache entry for the data.
createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
listItems: build.query<string[], number>({
query: (pageNumber) => `/listItems?page=${pageNumber}`,
// Only have one cache entry because the arg always maps to one string
serializeQueryArgs: ({ endpointName }) => {
return endpointName
},
// Always merge incoming data to the cache entry
merge: (currentCache, newItems) => {
currentCache.push(...newItems)
},
// Refetch when the page arg changes
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg
},
}),
}),
})
Source: RTK Documenation on the merge option
Using this you can easily implement infinite scroll. Changing the pageNumber parameter of your query, will automatically fetch new data and concat it with the data that was already in the cache.
To illustrate this, I've created a working example on CodeSandbox.
Here is a workaround for having infinite loading with caching benefits of rtk-query
in order to make it work properly when invalidating tag I had to fetch first page with hook and handle the rest in useEffect.
import { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import * as R from 'ramda';
import { ApiEndpointQuery } from '#reduxjs/toolkit/dist/query/core/module';
import { QueryHooks } from '#reduxjs/toolkit/dist/query/react/buildHooks';
interface UseLazeyInfiniteDataProps<T, N> {
api: T;
/** any rtk-query api: passing the whole enpoint so we have access to api utils to invalidate provided tags */
apiEndpointName: N;
/** apiEndpoint name to retrieve correct apiEndpoint query which will have 'initiate' and 'useQuery' */
apiArgs: { [key: string]: any; params: object };
/** apiArgs are the query arguments it should have a params objec */
limit?: number;
/** limit or page-size per request (defaults 20) */
invalidatesTags?: any[];
}
/**
* This hook is for having infinite loading experience with caching posibility of rtk-query
* it's storing the data comming from rtk-q to local useState throgh a useEffect hook
* in orther to make it work when invalidating tags it makes the first page request through rtk-query hook
* and whenever it changes it will refetch the rest data
*/
const useLazyInfiniteData = <
T extends { endpoints: any; util: any },
N extends keyof T['endpoints'],
>({
api,
apiEndpointName,
apiArgs,
limit = 20,
invalidatesTags,
}: UseLazeyInfiniteDataProps<T, N>) => {
const dispatch = useDispatch<any>();
const [pageNumber, setPageNumber] = useState(1); // first load only page 1
const [maxPage, setMaxPage] = useState(0); // we don't know how many pages could exists yet
const [accData, setAccData] = useState<any[]>([]);
const [isFetchingMore, setIsFetchingMore] = useState(false);
const apiEndpoint: ApiEndpointQuery<any, any> & QueryHooks<any> =
api.endpoints[apiEndpointName];
// we need this extra hook to automate refetching when invalidating tag
// this will make the useEffect rerender if the first page data changes
const {
currentData: firstPageData,
isLoading,
isFetching,
refetch: refetch_,
} = apiEndpoint.useQuery({
...apiArgs,
params: R.mergeRight(apiArgs.params, { offset: 0, limit }),
});
const refetch = useCallback(() => {
if (invalidatesTags) {
dispatch(api.util.invalidateTags());
}
refetch_();
}, [api.util, dispatch, invalidatesTags, refetch_]);
/** when params change like changing filters in the params then we reset the loading pages to 1 */
useEffect(
function resetPageLoadDataForSinglePage() {
setPageNumber(1);
},
[apiArgs.params],
);
useEffect(
function loadMoreDataOnPageNumberIncrease() {
if (firstPageData)
setMaxPage(Math.ceil((firstPageData as any).count / limit));
if (pageNumber === 1) {
setAccData((firstPageData as any)?.items ?? []);
}
if (pageNumber > 1) {
setIsFetchingMore(true);
const promises = R.range(1, pageNumber).map((page) =>
dispatch(
apiEndpoint.initiate({
...apiArgs,
params: R.mergeRight(apiArgs.params, {
offset: page * limit,
limit,
}),
}),
).unwrap(),
);
Promise.all(promises)
.then((data: any[]) => {
const items = R.chain(R.propOr([], 'items'), [
firstPageData,
...data,
]);
setAccData(items);
})
.catch(console.error)
.finally(() => {
setIsFetchingMore(false);
});
}
},
[apiEndpoint, apiArgs, dispatch, firstPageData, limit, pageNumber],
);
/** increasing pageNumber will make the useEffect run */
const loadMore = useCallback(() => {
setPageNumber(R.inc);
}, []);
return {
data: accData,
loadMore,
hasMore: pageNumber < maxPage,
isLoading,
isFetching,
isFetchingMore,
refetch,
};
};
export default useLazyInfiniteData;

How can I use Apollo's cacheRedirect with a nested query

I've got a query that looks like this:
export const GET_PROJECT = gql`
query GetProject($id: String!) {
homework {
getProject(id: $id) {
...ProjectFields
}
}
}
${ProjectFieldsFragment}
`;
My InMemoryCache looks like this:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
getProject: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
The above cache redirect is never hit. However, if I modify it to look like:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
homework: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
It does get hit, however I don't have any of the arguments that are passed in the nested getProject query. What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for, like:
export const SESSION = gql`
query Session {
session {
user {
id
fullName
email
}
organizations {
name
id
}
}
}
`;
So what is going on? I've resorted to just using readFragment in the places where I want the cache to redirect, but I'd like for that logic to become centralized.
It's hard to say for sure with these kinds of issues, but I'm betting that, since you say
What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for
the issue might be with your dataIdFromObject function.
This function is ultimately what decides if data is read from the cache or not. You should only override this if you have a very specific reason to. For example:
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
// ...
export default new ApolloClient({
link,
cache: new InMemoryCache({
dataIdFromObject(object) {
switch (object.__typename) {
case 'ModifierScale':
case 'ModifierGroup':
return [
object.__typename,
object.id,
...object.defaults
.map((defaultModifier) => defaultModifier.id)
.join(''),
].join('');
default:
return defaultDataIdFromObject(object); // fall back to default handling
}
},
}),
});
The point of this setting is to allow you to customize the key that gets put into the cache when you are loading the data.
If this doesn't solve your issue, I would definitely go into the Apollo tab in the chrome dev tools (you need the Apollo dev tools chrome extension to do this) and look at the cache section. It should show you the data in the cache and the key that the data is stored in.

Apollo useQuery() - "refetch" is ignored if the response is the same

I am trying to use Apollo-client to pull my users info and stuck with this problem:
I have this Container component responsible for pulling the user's data (not authentication) once it is rendered. User may be logged in or not, the query returns either viewer = null or viewer = {...usersProps}.
Container makes the request const { data, refetch } = useQuery<Viewer>(VIEWER);, successfully receives the response and saves it in the data property that I use to read .viewer from and set it as my current user.
Then the user can log-out, once they do that I clear the Container's user property setUser(undefined) (not showed in the code below, not important).
The problem occurred when I try to re-login: Call of refetch triggers the graphql http request but since it returns the same data that was returned during the previous initial login - useQuery() ignores it and does not update data. Well, technically there could not be an update, the data is the same. So my code setUser(viewer); does not getting executed for second time and user stucks on the login page.
const { data, refetch } = useQuery<Viewer>(VIEWER);
const viewer = data && data.viewer;
useEffect(() => {
if (viewer) {
setUser(viewer);
}
}, [ viewer ]);
That query with the same response ignore almost makes sense, so I tried different approach, with callbacks:
const { refetch } = useQuery<Viewer>(VIEWER, {
onCompleted: data => {
if (data.viewer) {
setUser(data.viewer);
}
}
});
Here I would totally expect Apollo to call the onCompleted callback, with the same data or not... but it does not do that. So I am kinda stuck with this - how do I make Apollo to react on my query's refetch so I could re-populate user in my Container's state?
This is a scenario where apollo's caches come handy.
Client
import { resolvers, typeDefs } from './resolvers';
let cache = new InMemoryCache()
const client = new ApolloClient({
cache,
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
headers: {
authorization: localStorage.getItem('token'),
},
}),
typeDefs,
resolvers,
});
cache.writeData({
data: {
isLoggedIn: !!localStorage.getItem('token'),
cartItems: [],
},
})
LoginPage
const IS_LOGGED_IN = gql`
query IsUserLoggedIn {
isLoggedIn #client
}
`;
function IsLoggedIn() {
const { data } = useQuery(IS_LOGGED_IN);
return data.isLoggedIn ? <Pages /> : <Login />;
}
onLogin
function Login() {
const { data, refetch } = useQuery(LOGIN_QUERY);
let viewer = data && data.viewer
if (viewer){
localStorage.setItem('token',viewer.token)
}
// rest of the stuff
}
onLogout
onLogout={() => {
client.writeData({ data: { isLoggedIn: false } });
localStorage.clear();
}}
For more information regarding management of local state. Check this out.
Hope this helps!

Apollo GraphQL: Lazy loading data after SSR

I have the following code:
const HomeWithApollo = withApollo(compose(
graphql(HOME_QUERY, {
props({
data: { loading, page, fetchMore }
}) {
return {
loading,
page,
fetchLocations: () => {
return fetchMore({
query: ALL_LOCATIONS_QUERY,
updateQuery: (prev, {fetchMoreResult}) => {
if (!fetchMoreResult.data) { return prev; }
return {
data: [...prev, ...fetchMoreResult.data]
};
}
})
}
};
}
})
)(Home));
Before migrating to Apollo I loaded the ALL_LOCATIONS_QUERY as an isomorphic fetch client-side (basically an AJAX request). But I'm looking for the Apollo way and I'm not sure if I have it yet. I have a few questions.
Using graphql(QUERY_NAME, { options }), since I'm loading Home data and Locations data completely separately, should they be placed in separate, multiple graphql functions within the withApollo(compose([...here]) Higher Order Component?
Currently to get data from the fetchMore function I am doing the following, but something tells me that it should be done within state so Apollo is aware of it for caching. Any thoughts on this? Am I moving in the right direction?
async componentDidMount(){
const { data } = await this.props.fetchLocations();
this.setState({ locations: data.allLocations });
}
Thank you very much in advance!

How do you dynamically control react apollo-client query initiation?

A react component wrapped with an apollo-client query will automatically initiate a call to the server for data.
I would like to fire off a request for data only on a specific user input.
You can pass the skip option in the query options - but this means the refetch() function is not provided as a prop to the component; and it appears that the value of skip is not assessed dynamically on prop update.
My use is case is a map component. I only want data for markers to be loaded when the user presses a button, but not on initial component mount or location change.
A code sample below:
// GraphQL wrapping
Explore = graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
skip: ({ targetResource, searchIsAllowedForMapArea }) => {
const skip = Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE');
return skip;
},
}),
props: ({ ownProps, data: { loading, viewer, refetch }}) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})(Explore);
To include an HoC based on a condition affected by a props change, you could use branch from recompose.
branch(
test: (props: Object) => boolean,
left: HigherOrderComponent,
right: ?HigherOrderComponent
): HigherOrderComponent
check: https://github.com/acdlite/recompose/blob/master/docs/API.md#branch
For this specific example, would look something like:
const enhance = compose(
branch(
// evaluate condition
({ targetResource, searchIsAllowedForMapArea }) =>
Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE'),
// HoC if condition is true
graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
}),
props: ({ ownProps, data: { loading, viewer, refetch } }) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})
)
);
Explore = enhance(Explore);
I have a similar use case, I wanted to load the data only when the user clicked.
I've not tried the withQuery suggestion given by pencilcheck above. But I've seen the same suggestion elsewhere. I will try it, but in the meantime this is how I got it working based off a discussion on github:
./loadQuery.js
Note: I'm using the skip directive:
const LOAD = `
query Load($ids:[String], $skip: Boolean = false) {
things(ids: $ids) #skip(if: $skip) {
title
}
`
LoadMoreButtonWithQuery.js
Here I use the withState higher-order function to add in a flag and a flag setter to control skip:
import { graphql, compose } from 'react-apollo';
import { withState } from 'recompose';
import LoadMoreButton from './LoadMoreButton';
import LOAD from './loadQuery';
export default compose(
withState('isSkipRequest', 'setSkipRequest', true),
graphql(
gql(LOAD),
{
name: 'handleLoad',
options: ({ids, isSkipRequest}) => ({
variables: {
ids,
skip: isSkipRequest
},
})
}
),
)(Button);
./LoadMoreButton.js
Here I have to manually "flip" the flag added using withState:
export default props => (
<Button onClick={
() => {
props.setSkipRequest(false); // setter added by withState
props.handleLoad.refetch();
}
}>+</Button>
);
Frankly I'm a little unhappy with this, as it is introduces a new set of wiring (composed in by "withState"). Its also not battle tested - I just got it working and I came to StackOverflow to check for better solutions.

Resources