Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

docs: Add more extensive infinite pagination example code (#3258)

Changed files
+344 -6
docs
examples
+4 -2
docs/basics/ui-patterns.md
···
Here we keep an array of all `variables` we've encountered and use them to render their
respective `result` page. This only rerenders the additional page rather than having a long
-
list that constantly changes. [You can find a full code example of this pattern in our example folder on the topic of Graphcache pagination.](https://github.com/urql-graphql/urql/tree/main/examples/with-graphcache-pagination)
+
list that constantly changes. [You can find a full code example of this pattern in our example folder on the topic of pagination.](https://github.com/urql-graphql/urql/tree/main/examples/with-pagination)
-
We also do not need to use our normalized cache to achieve this. As long as we're able to split individual lists up into chunks across components, we can also solve this problem entirely in UI code. [Read our example code on how to achieve this.](https://github.com/urql-graphql/urql/tree/main/examples/with-pagination)
+
This code doesn't take changing variables into account, which will affect the cursors. For an
+
example that takes full infinite scrolling into account, [you can find a full code example of an
+
extended pattern in our example folder on the topic of infinite pagination.](https://github.com/urql-graphql/urql/tree/main/examples/with-infinite--pagination)
## Prefetching data
+8
docs/graphcache/local-resolvers.md
···
with separate components per page in environments like React Native, where a `FlatList` would
require a flat, infinite list of items.
+
> **Note:** If you don't need a flat array of results, you can also achieve infinite pagination
+
> with only UI code. [You can find a code example of UI infinite pagination in our example folder.](https://github.com/urql-graphql/urql/tree/main/examples/with-pagination)
+
+
[You can find a code example of infinite pagination with Graphcahce in our example folder.](https://github.com/urql-graphql/urql/tree/main/examples/with-graphcache-pagination).
+
Please keep in mind that this patterns has some limitations when you're handling cache updates.
+
Deleting old pages from the cache selectively may be difficult, so the UI pattern in the above
+
note is preferred.
+
### Simple Pagination
Given we have a schema that uses some form of `offset` and `limit` based pagination, we can use the
+3 -1
examples/README.md
···
| [`with-svelte`](./with-svelte) | Shows a basic query in `@urql/svelte` with Svelte. |
| [`with-vue3`](./with-vue3) | Shows a basic query in `@urql/vue` with Vue 3. |
| [`with-next`](./with-next) | Shows some examples with `next-urql` in Next.js with the default, `getStaticProps` and `getServerSideProps`. |
-
| [`with-pagination`](./with-pagination) | Shows how to generically set up infinite pagination with `urql` in UI code. |
+
| [`with-pagination`](./with-pagination) | Shows how to generically set up pagination with `urql` in UI code. |
+
| [`with-infinite-pagination`](./with-infinite-pagination) | Shows how to generically set up infinite scrolling pagination with `urql` in UI code. |
| [`with-apq`](./with-apq) | Shows Automatic Persisted Queries with `@urql/exchange-persisted-fetch`. |
| [`with-graphcache-updates`](./with-graphcache-updates) | Shows manual cache updates with `@urql/exchange-graphcache`. |
| [`with-graphcache-pagination`](./with-graphcache-pagination) | Shows the automatic infinite pagination helpers from `@urql/exchange-graphcache`. |
···
| [`with-refresh-auth`](./with-refresh-auth) | Shows an example of authentication with refresh tokens using `@urql/exchange-auth`. |
| [`with-retry`](./with-retry) | Shows how to set up `@urql/exchange-retry` for retrying failed operations. |
| [`with-defer-stream-directives`](./with-defer-stream-directives) | Demonstrates `urql` and `@urql/exchange-graphcache` with built-in support for `@defer` and `@stream`. |
+
| [`with-subscriptions-via-fetch`](./with-subscriptions-via-fetch) | Demonstrates `urql` executing subscriptions with a GraphQL Yoga API via the `fetchExchange`. |
+41
examples/with-infinite-pagination/README.md
···
+
# With Infinite Pagination (in React)
+
+
This example shows how to implement **infinite scroll** pagination with `urql`
+
in your React UI code.
+
+
It's slightly different than the [`with-pagination`](../with-pagination) example
+
and shows how to implement a full infinitely scrolling list with only your UI code,
+
while fulfilling the following requirements:
+
+
- Unlike with [`with-graphcache-pagination`](../with-graphcache-pagination),
+
the `urql` cache doesn't have to know about your infinite list, and this works
+
with any cache, even the document cache
+
- Unlike with [`with-pagination`](../with-pagination), your list can use cursors,
+
and each page can update, while keeping the variables for the next page dynamic.
+
- It uses no added state, no extra processing of lists, and you need no effects.
+
+
In other words, unless you need a flat array of items
+
(e.g. unless you’re using React Native’s `FlatList`), this is the simplest way
+
to implement an infinitely scrolling, paginated list.
+
+
This example is also reapplicable to other libraries, like Svelte or Vue.
+
+
To run this example install dependencies and run the `start` script:
+
+
```sh
+
yarn install
+
yarn run start
+
# or
+
npm install
+
npm run start
+
```
+
+
This example contains:
+
+
- The `urql` bindings and a React app with a client set up in [`src/App.js`](src/App.jsx)
+
- This also contains a search input which is used as input for the GraphQL queries
+
- All pagination components are in [`src/SearchResults.jsx`](src/SearchResults.jsx)
+
- The `SearchRoot` component loads the first page of results and renders `SearchPage`
+
- The `SearchPage` displays cached results, and otherwise only starts a network request on
+
a button press
+
- The `Package` component is used for each result item
+27
examples/with-infinite-pagination/index.html
···
+
<!DOCTYPE html>
+
<html lang="en">
+
<head>
+
<meta charset="UTF-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
<title>with-pagination</title>
+
<style>
+
body {
+
margin: 0;
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+
sans-serif;
+
-webkit-font-smoothing: antialiased;
+
-moz-osx-font-smoothing: grayscale;
+
}
+
+
code {
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+
monospace;
+
}
+
</style>
+
</head>
+
<body>
+
<div id="root"></div>
+
<script type="module" src="/src/index.jsx"></script>
+
</body>
+
</html>
+19
examples/with-infinite-pagination/package.json
···
+
{
+
"name": "with-pagination",
+
"version": "0.0.0",
+
"private": true,
+
"scripts": {
+
"start": "vite"
+
},
+
"dependencies": {
+
"@urql/core": "^4.0.10",
+
"graphql": "^16.6.0",
+
"react": "^18.2.0",
+
"react-dom": "^18.2.0",
+
"urql": "^4.0.3"
+
},
+
"devDependencies": {
+
"@vitejs/plugin-react": "^3.1.0",
+
"vite": "^4.2.0"
+
}
+
}
+52
examples/with-infinite-pagination/src/App.jsx
···
+
import React, { useState } from 'react';
+
import { Client, Provider, cacheExchange, fetchExchange } from 'urql';
+
+
import SearchRoot from './SearchResults';
+
+
const client = new Client({
+
// The GraphQL API we use here uses the NPM registry
+
// We'll use it to display search results for packages
+
url: 'https://trygql.formidable.dev/graphql/relay-npm',
+
exchanges: [cacheExchange, fetchExchange],
+
});
+
+
// We will be able to enter a search term, and this term
+
// will render search results
+
function PaginatedNpmSearch() {
+
const [search, setSearch] = useState('urql');
+
+
const setSearchValue = event => {
+
event.preventDefault();
+
setSearch(event.currentTarget.value);
+
};
+
+
return (
+
<>
+
<header>
+
<h4>Type to search for npm packages</h4>
+
{/* Try changing the search input, then changing it back... */}
+
{/* If you do this, all cached pages will display immediately! */}
+
<input
+
type="search"
+
value={search}
+
onChange={setSearchValue}
+
placeholder="npm package name"
+
/>
+
</header>
+
<main>
+
{/* The <SearchRoot> component contains all querying logic */}
+
<SearchRoot searchTerm={search} />
+
</main>
+
</>
+
);
+
}
+
+
function App() {
+
return (
+
<Provider value={client}>
+
<PaginatedNpmSearch />
+
</Provider>
+
);
+
}
+
+
export default App;
+172
examples/with-infinite-pagination/src/SearchResults.jsx
···
+
import React, { useCallback } from 'react';
+
import { gql, useQuery } from 'urql';
+
+
// We define a fragment, just to define the data
+
// that our item component will use in the results list
+
const packageFragment = gql`
+
fragment SearchPackage on Package {
+
id
+
name
+
latest: version(selector: "latest") {
+
version
+
}
+
}
+
`;
+
+
// The main query fetches the first page of results and gets our `PageInfo`
+
// This tells us whether more pages are present which we can query.
+
const rootQuery = gql`
+
query SearchRoot($searchTerm: String!, $resultsPerPage: Int!) {
+
search(query: $searchTerm, first: $resultsPerPage) {
+
edges {
+
cursor
+
node {
+
...SearchPackage
+
}
+
}
+
pageInfo {
+
hasNextPage
+
endCursor
+
}
+
}
+
}
+
+
${packageFragment}
+
`;
+
+
// We split the next pages we load into a separate query. In this example code,
+
// both queries could be the same, but we keep them separate for educational
+
// purposes.
+
// In a real app, your "root query" would often fetch more data than the search page query.
+
const pageQuery = gql`
+
query SearchPage(
+
$searchTerm: String!
+
$resultsPerPage: Int!
+
$afterCursor: String!
+
) {
+
search(query: $searchTerm, first: $resultsPerPage, after: $afterCursor) {
+
edges {
+
cursor
+
node {
+
...SearchPackage
+
}
+
}
+
pageInfo {
+
hasNextPage
+
endCursor
+
}
+
}
+
}
+
+
${packageFragment}
+
`;
+
+
// This is the <SearchRoot> component that we render in `./App.jsx`.
+
// It accepts our variables as props.
+
const SearchRoot = ({ searchTerm = 'urql', resultsPerPage = 10 }) => {
+
const [rootResult] = useQuery({
+
query: rootQuery,
+
variables: {
+
searchTerm,
+
resultsPerPage,
+
},
+
});
+
+
if (rootResult.fetching) {
+
return <em>Loading...</em>;
+
}
+
+
// Here, we render the results as a list into a fragment, and if `hasNextPage`
+
// is truthy, we immediately render <SearchPage> for the next page.
+
const connection = rootResult.data?.search;
+
return (
+
<>
+
{connection?.edges?.length === 0 ? <strong>No Results</strong> : null}
+
+
{connection?.edges.map(edge => (
+
<Package key={edge.cursor} node={edge.node} />
+
))}
+
+
{/* The <SearchPage> component receives the same props, plus the `afterCursor` for its variables */}
+
{connection?.pageInfo.hasNextPage ? (
+
<SearchPage
+
searchTerm={searchTerm}
+
resultsPerPage={resultsPerPage}
+
afterCursor={connection.pageInfo.endCursor}
+
/>
+
) : rootResult.fetching ? (
+
<em>Loading...</em>
+
) : null}
+
</>
+
);
+
};
+
+
// The <SearchPage> is rendered for each page of results, except for the root query.
+
// It renders *itself* recursively, for the next page of results.
+
const SearchPage = ({ searchTerm, resultsPerPage, afterCursor }) => {
+
// Each <SearchPage> fetches its own page results!
+
const [pageResult, executeQuery] = useQuery({
+
query: pageQuery,
+
// Initially, we *only* want to display results if, they're cached
+
requestPolicy: 'cache-only',
+
// We don't want to run the query if we don't have a cursor (in this example, this will never happen)
+
pause: !afterCursor,
+
variables: {
+
searchTerm,
+
resultsPerPage,
+
afterCursor,
+
},
+
});
+
+
// We only load more results, by allowing the query to make a network request, if
+
// a button has pressed.
+
// In your app, you may want to do this automatically if the user can see the end of
+
// your list, e.g. via an IntersectionObserver.
+
const onLoadMore = useCallback(() => {
+
// This tells the query above to execute and instead of `cache-only`, which forbids
+
// network requests, we now allow them.
+
executeQuery({ requestPolicy: 'cache-first' });
+
}, [executeQuery]);
+
+
if (pageResult.fetching) {
+
return <em>Loading...</em>;
+
}
+
+
const connection = pageResult.data?.search;
+
return (
+
<>
+
{/* If our query has nodes, we render them here. The page renders its own results */}
+
{connection?.edges.map(edge => (
+
<Package key={edge.cursor} node={edge.node} />
+
))}
+
+
{/* If we have a next page, we now render it recursively! */}
+
{/* As before, the next <SearchPage> will not fetch immediately, but only query from cache */}
+
{connection?.pageInfo.hasNextPage ? (
+
<SearchPage
+
searchTerm={searchTerm}
+
resultsPerPage={resultsPerPage}
+
afterCursor={connection.pageInfo.endCursor}
+
/>
+
) : pageResult.fetching ? (
+
<em>Loading...</em>
+
) : null}
+
+
{!connection && !pageResult.fetching ? (
+
<button type="button" onClick={onLoadMore}>
+
Load more
+
</button>
+
) : null}
+
</>
+
);
+
};
+
+
// This is the component that then renders each result item
+
const Package = ({ node }) => (
+
<section>
+
<strong>{node.name}</strong>
+
<em>@{node.latest.version}</em>
+
</section>
+
);
+
+
export default SearchRoot;
+6
examples/with-infinite-pagination/src/index.jsx
···
+
import React from 'react';
+
import { createRoot } from 'react-dom/client';
+
+
import App from './App';
+
+
createRoot(document.getElementById('root')).render(<App />);
+7
examples/with-infinite-pagination/vite.config.js
···
+
import { defineConfig } from 'vite';
+
import react from '@vitejs/plugin-react';
+
+
// https://vitejs.dev/config/
+
export default defineConfig({
+
plugins: [react()],
+
});
+5 -3
examples/with-pagination/README.md
···
# With Pagination (in React)
-
This example shows how to implement infinite pagination with `urql` in your React UI code. It
-
renders several pages as fragments with one component managing the variables for the page queries.
-
This example is also reapplicable to other libraries, like Svelte or Vue.
+
This example shows how to implement pagination with `urql` in your React UI code.
+
+
It renders several pages as fragments with one component managing the variables
+
for the page queries. This example is also reapplicable to other libraries,
+
like Svelte or Vue.
To run this example install dependencies and run the `start` script: