Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pagination Apollo v3 example #6502

Closed
yeomann opened this issue Jun 27, 2020 · 24 comments
Closed

Pagination Apollo v3 example #6502

yeomann opened this issue Jun 27, 2020 · 24 comments

Comments

@yeomann
Copy link

yeomann commented Jun 27, 2020

Hi,

I am not sure what is the correct place to ask, i am confused sorry in advance, asked already here as well apollographql/apollo-link#1290

I'm using fetchmore updateQueries but getting warning in console that updateQueries is maybe depreciated.

invariant.ts:30 The updateQuery callback for fetchMore is deprecated, and will be removed
in the next major version of Apollo Client.

Please convert updateQuery functions to field policies with appropriate
read and merge functions, or use/adapt a helper function (such as
concatPagination, offsetLimitPagination, or relayStylePagination) from
@apollo/client/utilities.

The field policy system handles pagination more effectively than a
hand-written updateQuery function, and you only need to define the policy
once, rather than every time you call fetchMore.

İ have searched on documents enough but i don't see any example how to implement offset pagination without using updateQueries method.

Could you provide a bare simple example please
Many thanks in advance.

@mschipperheyn
Copy link

Check this out: 48df9da

@onpaws
Copy link

onpaws commented Jun 30, 2020

I don't claim to understand how but I'm happy to report I was able to get Relay pagination working using @benjamn 's recent efforts in 3.0.0-rc.9. For my case I left the keyArg param off, like so:

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        foods: relayStylePagination(),
      },
    },
  },
})

Been trying to learn InMemoryCache 3.0 and while I don't have a strong handle on it yet, at least today it seems pretty convenient to have Apollo handle pagination 'automatically' vs a bunch of separate places in my app using largely the same boilerplate.

@majorhayes
Copy link

If they are going to deprecate a feature, they should update the documentation to show the new way of doing things. I saw the same error and can't find "field policies" in the documentation at all.

@dylanwulf
Copy link
Contributor

dylanwulf commented Jul 16, 2020

Pagination docs are definitely out of date. For now, you can find a little more info about new pagination patterns on this blog post: https://www.apollographql.com/blog/announcing-the-release-of-apollo-client-3-0/?mc_cid=e593721cc7&mc_eid=235a3fce14

Implementation of these new pagination policies can be found here: https://github.com/apollographql/apollo-client/blob/master/src/utilities/policies/pagination.ts

@GABAnich
Copy link

GABAnich commented Jul 21, 2020

If they are going to deprecate a feature, they should update the documentation to show the new way of doing things. I saw the same error and can't find "field policies" in the documentation at all.

https://www.apollographql.com/docs/react/caching/cache-field-behavior/#the-merge-function

@GABAnich
Copy link

https://www.apollographql.com/docs/react/migrating/apollo-client-3-migration/#using-apollo-utilities-without-the-rest-of-apollo-client

@GABAnich
Copy link

https://github.com/apollographql/apollo-client/blob/master/src/utilities/policies/pagination.ts

@smaven
Copy link

smaven commented Jul 22, 2020

Not sure if it's a bug but while using relayStylePagination() component does an initial render without loading in data and returns the default value set by makeEmptyData().

In order to work around this issue I adjusted the 'read' function as:

read(existing, { canRead }) {
	if (!existing) return;
	const edges = existing.edges.filter((edge) => canRead(edge.node));
	return {
		// Some implementations return additional Connection fields, such
		// as existing.totalCount. These fields are saved by the merge
		// function, so the read function should also preserve them.
		...existing,
		edges,
		pageInfo: {
			...existing.pageInfo,
			startCursor: cursorFromEdge(edges, 0),
			endCursor: cursorFromEdge(edges, -1),
		},
	};
},

@uncvrd
Copy link

uncvrd commented Jul 22, 2020

@Salman-Maven wow great timing, I just ran in to this tonight as well. If you check your network tab you'll notice there isn't a network request on page load for your connection data. If I change my fetchPolicy to something that forces a network request like cache-and-network, then it'll load. Here's a gif showing the issue. In this case I have a swipe down to refresh feature that forces a network request using the refetch.

Note that the graphql XHR you see on page load is not related to the loading of my connection data. I definitely think this is a bug

Alt Text

@PascalHelbig
Copy link

@Salman-Maven: thanks, your workaround works for me.

I used your solution to create a custom field policy, which I will use for all my relay connections as long as this bug exists:

import { relayStylePagination } from '@apollo/client/utilities';
import { FieldPolicy } from '@apollo/client/cache';

const fixedRelayStylePagination = (...args): FieldPolicy => ({
  ...relayStylePagination(...args),
  read: (...readArgs) => {
    const existing = readArgs[0];
    const originalRead = relayStylePagination(...args).read;

    if (!existing || !originalRead) {
      return;
    }
    return originalRead(...readArgs);
  },
});

usage:

new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        chat: fixedRelayStylePagination(),
      },
    },
  },
});

@smaven
Copy link

smaven commented Jul 23, 2020

@uncvrd the gif sums up the issue perfectly. Changing the fetchPolicy to cache-and-network does force a network request. However, it will change your app behavior and you will be sending an extra request every time the component gets mounted (which might be unnecessary in some cases).

Also in my case, I was using server-side rendering so I couldn't use cache-and-network with client.query. While debugging the relayStylePagination(), I also noticed that no request was being sent and the read() was returning a default value from makeEmptyData(). Simply returning from the read() when there is no existing data found seemed to solve it.

For now, I'm using the relayStylePagination() helper with a modified read() as mentioned above. The custom field policy that @PascalHelbig used also works great!

@benjamn
Copy link
Member

benjamn commented Jul 24, 2020

Please try @apollo/[email protected], which includes #6684, which I think may be related to some of these issues.

@crwecker
Copy link

I also have questions about converting to use field policies instead of updateQueries. My current Query/fetchMore looks like this:

const DiscoverQuery = gql`
  query DiscoverRows ($limit: Int = 4, $cursor: String) {
    viewer {
      discoveryItemsConnection(first: $limit, after: $cursor) @connection(key: "discoverItems") {
        pageInfo {
          hasNextPage
          endCursor
        }
        discoveryItems {
          ...on SpecialList {
            title
            specialsConnection {
              specials {
                id
                source {
                  id
                  lastSession {
                    percentWatched
                    location(format: CLOCK)
                    locationRaw: location(format: RAW)
                  }
                }
                title
                poster { uri: url }
              }
            }
          }
        }
      }
    }
  }
`
          fetchMore({
            variables: {
              limit: 8,
              cursor: endCursor
            },
            updateQuery: (previousResult, { fetchMoreResult }) => {
              const { discoveryItems, pageInfo } = fetchMoreResult.viewer?.discoveryItemsConnection ?? {}
              return discoveryItems.length
                ? {
                  ...previousResult,
                  viewer: {
                    ...previousResult.viewer,
                    discoveryItemsConnection: {
                      ...previousResult.viewer.discoveryItemsConnection,
                      discoveryItems: [...previousResult.viewer.discoveryItemsConnection.discoveryItems, ...discoveryItems],
                      pageInfo
                    }
                  }
                }
                : previousResult
            }
          })

So, I would start by reading up on how field policies work here:
https://www.apollographql.com/docs/react/caching/cache-field-behavior/
Then probably look at the cache policy helper functions like offsetLimitPagination and relayStylePagination in @apollo/client/utilities (https://github.com/apollographql/apollo-client/blob/master/src/utilities/policies/pagination.ts) I don't think any of them actually fit my situation.
Then write my own field policy using read and merge functions using my limit and endCursor parameters.

export function cursorLimitPagination() {
  return {
      read: function (existing, _a) {
        // Add code here that I don't understand yet but that returns the data and pageInfo
      },
      merge: function (existing, incoming, _a) {
        // Add code here that I don't understand yet but that returns the merged data and pageInfo
      },
  };
}

Then I'd add that to the InMemoryCache constructor. In my case it might look like something like this:

// This is the same cache you pass into new ApolloClient
const cache = new InMemoryCache({
  possibleTypes,
  typePolicies: { // Type policy map
    Query: {
      fields: { // Field policy map for the Query type
        viewer: {
          discoveryItemsConnection: cursorLimitPagination()
        }
      }
    }
  }
})

Does that sound right?

mailtime-wing added a commit to mailtime-wing/rewardme-mobile that referenced this issue Aug 31, 2020
@CodySwannGT
Copy link

This is a really weird implementation choice. We use Apollo across a number of projects and have a standard updateQuery for paginating data sets.

So now we have to go back through all our projects and add field-level information for all our data models?

@mingjuitsai
Copy link

mingjuitsai commented Oct 5, 2020

What if you have a field under a Query that you want to merge the data one way, but you want it to merge differently on another list? Same query but merge differently

@tlaverdure
Copy link

tlaverdure commented Feb 18, 2021

What if you have a field under a Query that you want to merge the data one way, but you want it to merge differently on another list? Same query but merge differently

This is exactly one of the roadblocks that I've run into. For example, I use fetchMore() to get more notifications and then subscribeToMore() to get new notifications from a Subscription. I would like to merge the the new notification so that it is the first in the list. However, using the updateQuery callback in conjunction with subscribeToMore() has the side effect of merging that return value with the type policy merge function. So if I had 10 notifications, I now have 10 + 11.

I honestly would rather handle merging in multiple places using updateQuery than being restricted to the limitations of type policy merge functions. Am I missing something here?

I'd also like to know why the updateQuery callback is still being suggested here? https://www.apollographql.com/docs/react/caching/advanced-topics/#incremental-loading-fetchmore

And why is the updateQuery callback is still acceptable in subscribeToMore()?
https://www.apollographql.com/docs/react/data/subscriptions/#subscribing-to-updates-for-a-query

@Tushant
Copy link

Tushant commented Mar 2, 2021

In my case, the incoming data is not merged with existing data. When i click on load more button, it presents only the current data.

This is how I have done

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        conversation: relayStylePagination(),
       // conversation: {
//     keyArgs: [],
//     merge(existing = {}, incoming, { args }) {
//       console.log('existing', existing);
//       console.log('incominng', incoming);
//       if (args && !args.params.after) {
//         return incoming;
//       }
//       return {
//         ...existing,
//         pageInfo: incoming.data.pageInfo,
//         edges: [...existing.data.edges, ...incoming.data.edges],
//       };
//     },
//   },
      },
    },
  },
});
const first = 10;

export default function ChatBox(): ReactElement {
  const { channelId, contactId } = useParams<Record<string, string | undefined>>();
  const { data, fetchMore, networkStatus, subscribeToMore } = useQuery<Pick<Query, 'conversation'>>(
    CONVERSATION,
    {
      variables: {
        channel: channelId,
        contact: contactId,
        params: {
          first,
        },
      },
      fetchPolicy: 'cache-and-network',
      nextFetchPolicy: 'cache-only',
    },
  );

  const messages = data?.conversation?.data?.edges;
  const hasNextPage = data?.conversation?.data?.pageInfo.hasNextPage;
  const after = data?.conversation?.data?.pageInfo.endCursor;
  const isRefetching = networkStatus === 3;

  const onLoadNext = () => {
    fetchMore({
      variables: {
        params: {
          first,
          after,
        },
      },
    });
  };

  console.log('messages', messages);
  return (
    <>
      <div className='d-flex flex-column h-100'>
        <TopBar />
        {!messages?.length ? (
          <EmptyConversation />
        ) : (
          <>
            <ChatStyles className='px-24' id='scroll'>
              <React.Suspense fallback={<Spinner />}>
                <MessageList messages={messages ? generateChatMessages(messages) : []} />
                {hasNextPage && (
                  <>
                    {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
                    <button
                      type='button'
                      id='buttonLoadMore'
                      disabled={isRefetching}
                      onClick={onLoadNext}
                    />
                  </>
                )}
              </React.Suspense>
            </ChatStyles>
          </>
        )}
      </div>
    </>
  );
}

this is the shape of query

conversation(
  channel: ShortId
  contact: ShortId
  params: ConnectionInput
): ConversationHistoryPayload!

type ConversationHistoryPayload {
  status: Int!
  error: ErrorData
  data: ConversationConnection
}
export const CONVERSATION = gql`
  query conversation($channel: ShortId, $contact: ShortId, $params: ConnectionInput) {
    conversation(channel: $channel, contact: $contact, params: $params) {
      status
      error {
        ...Error
      }
      data {
        pageInfo {
          ...PageInfo
        }
        edges {
          cursor
          node {
            ...Communication
          }
        }
      }
    }
  }
  ${ERROR_FRAGMENT}
  ${PAGE_INFO}
  ${COMMUNICATION}
`;

both relayStylePagination and merge did not work in my case. Did i miss anything?

@Tushant
Copy link

Tushant commented Mar 8, 2021

Since, the input type and response type is a bit different like in above case the arugmennts like first, after are inside params argument and pageInfo, edges are inside data object. So is it still possible to use relayStylePagination()? This would help me a lot.

@joshourisman
Copy link

I'm having the same issue as @Tushant. I can see that the fetchMore() call is successfully querying for the next page of data, and if I remove the ids from my query the merge function complains, suggesting it is actually merging in the new data, but I never get a re-render, so the new data does not show up, and the cursor isn't updated, so it only ever sits there fetching page 2 and never displaying the new data.

@hwillson
Copy link
Member

hwillson commented May 3, 2021

A lot has changed with @apollo/client; please refer to our updated pagination docs for help. Thanks!

@hwillson hwillson closed this as completed May 3, 2021
@ssamkough
Copy link

I'm having the same issue as @Tushant. I can see that the fetchMore() call is successfully querying for the next page of data, and if I remove the ids from my query the merge function complains, suggesting it is actually merging in the new data, but I never get a re-render, so the new data does not show up, and the cursor isn't updated, so it only ever sits there fetching page 2 and never displaying the new data.

Have you ever been able to resolve this?

@mathieuhasum
Copy link

Since, the input type and response type is a bit different like in above case the arugmennts like first, after are inside params argument and pageInfo, edges are inside data object. So is it still possible to use relayStylePagination()? This would help me a lot.

Hey,

We had the same issue and after investigating we found out that relayStylePagination() is actually checking for args.after internally.

Meaning that it won't work with your after as a subfield like params.after.

If you can't change your schema, you might be able to manually overwrite the arguments passed to relayStylePagination(...).merge 🤔

@Nightbr
Copy link

Nightbr commented Jul 22, 2021

I created an issue to find a solution to this problem (maybe pass settings to relayStylePagination to set the path to the paging object (paging, params, ...). -> #8524 (comment)

@benjamn
Copy link
Member

benjamn commented Jul 22, 2021

Thanks @Nightbr! Cross-posting my response #8524 (comment) in case it's useful to others. In short: please feel free to copy/paste/tweak/reimplement relayStylePagination however you see fit.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests