Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 183 lines 6.4 kB view raw view rendered
1--- 2title: UI-Patterns 3order: 6 4--- 5 6# UI Patterns 7 8> This page is incomplete. You can help us expanding it by suggesting more patterns or asking us about common problems you're facing on [GitHub Discussions](https://github.com/urql-graphql/urql/discussions). 9 10Generally, `urql`'s API surface is small and compact. Some common problems that we're facing when building apps may look like they're not a built-in feature, however, there are several patterns that even a lean UI can support. 11This page is a collection of common UI patterns and problems we may face with GraphQL and how we can tackle them in 12`urql`. These examples will be written in React but apply to any other framework. 13 14## Infinite scrolling 15 16"Infinite Scrolling" is the approach of loading more data into a page's list without splitting that list up across multiple pages. 17 18There are a few ways of going about this. In our [normalized caching chapter on the topic](../graphcache/local-resolvers.md#pagination) 19we see an approach with `urql`'s normalized cache, which is suitable to get started quickly. However, this approach also requires some UI code as well to keep track of pages. 20Let's have a look at how we can create a UI implementation that makes use of this normalized caching feature. 21 22```js 23import React from 'react'; 24import { useQuery, gql } from 'urql'; 25 26const PageQuery = gql` 27 query Page($first: Int!, $after: String) { 28 todos(first: $first, after: $after) { 29 nodes { 30 id 31 name 32 } 33 pageInfo { 34 hasNextPage 35 endCursor 36 } 37 } 38 } 39`; 40 41const SearchResultPage = ({ variables, isLastPage, onLoadMore }) => { 42 const [{ data, fetching, error }] = useQuery({ query: PageQuery, variables }); 43 const todos = data?.todos; 44 45 return ( 46 <div> 47 {error && <p>Oh no... {error.message}</p>} 48 {fetching && <p>Loading...</p>} 49 {todos && ( 50 <> 51 {todos.nodes.map(todo => ( 52 <div key={todo.id}> 53 {todo.id}: {todo.name} 54 </div> 55 ))} 56 {isLastPage && todos.pageInfo.hasNextPage && ( 57 <button onClick={() => onLoadMore(todos.pageInfo.endCursor)}>load more</button> 58 )} 59 </> 60 )} 61 </div> 62 ); 63}; 64 65const Search = () => { 66 const [pageVariables, setPageVariables] = useState([ 67 { 68 first: 10, 69 after: '', 70 }, 71 ]); 72 73 return ( 74 <div> 75 {pageVariables.map((variables, i) => ( 76 <SearchResultPage 77 key={'' + variables.after} 78 variables={variables} 79 isLastPage={i === pageVariables.length - 1} 80 onLoadMore={after => setPageVariables([...pageVariables, { after, first: 10 }])} 81 /> 82 ))} 83 </div> 84 ); 85}; 86``` 87 88Here we keep an array of all `variables` we've encountered and use them to render their 89respective `result` page. This only rerenders the additional page rather than having a long 90list 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) 91 92This code doesn't take changing variables into account, which will affect the cursors. For an 93example that takes full infinite scrolling into account, [you can find a full code example of an 94extended pattern in our example folder on the topic of infinite pagination.](https://github.com/urql-graphql/urql/tree/main/examples/with-infinite-pagination) 95 96## Prefetching data 97 98We sometimes find it necessary to load data for a new page before that page is opened, for instance while a JS bundle is still loading. We may 99do this with help of the `Client`, by calling methods without using the React bindings directly. 100 101```js 102import React from 'react'; 103import { useClient, gql } from 'urql'; 104 105const TodoQuery = gql` 106 query Todo($id: ID!) { 107 todo(id: $id) { 108 id 109 name 110 } 111 } 112`; 113 114const Component = () => { 115 const client = useClient(); 116 const router = useRouter(); 117 118 const transitionPage = React.useCallback(async id => { 119 const loadJSBundle = import('./page.js'); 120 const loadData = client.query(TodoQuery, { id }).toPromise(); 121 await Promise.all([loadJSBundle, loadData]); 122 router.push(`/todo/${id}`); 123 }, []); 124 125 return <button onClick={() => transitionPage('1')}>Go to todo 1</button>; 126}; 127``` 128 129Here we're calling `client.query` to prepare a query when the transition begins. 130We then call `toPromise()` on this query which activates it. Our `Client` and its cache share results, which means that we've already kicked off or even completed the query before we're on the new page. 131 132## Lazy query 133 134It's often required to "lazily" start a query, either at a later point or imperatively. This means that we don't start a query when a new component is mounted immediately. 135 136Parts of `urql` that automatically start, like the `useQuery` hook, have a concept of a [`pause` option.](./react-preact.md#pausing-usequery) This option is used to prevent the hook from automatically starting a new query. 137 138```js 139import React from 'react'; 140import { useQuery, gql } from 'urql'; 141 142const TodoQuery = gql` 143 query Todos { 144 todos { 145 id 146 name 147 } 148 } 149`; 150 151const Component = () => { 152 const [result, fetch] = useQuery({ query: TodoQuery, pause: true }); 153 const router = useRouter(); 154 155 return <button onClick={fetch}>Load todos</button>; 156}; 157``` 158 159We can unpause the hook to start fetching, or, like in this example, call its returned function to manually kick off the query. 160 161## Reacting to focus and stale time 162 163In urql we leverage our extensibility pattern named "Exchanges" to manipulate the way 164data comes in and goes out of our client. 165 166- [Stale time](https://github.com/urql-graphql/urql/tree/main/exchanges/request-policy) 167- [Focus](https://github.com/urql-graphql/urql/tree/main/exchanges/refocus) 168 169When we want to introduce one of these patterns we add the package and add it to the `exchanges` 170property of our `Client`. In the case of these two we'll have to add it before the cache 171else our requests will never get upgraded. 172 173```js 174import { Client, cacheExchange, fetchExchange } from 'urql'; 175import { refocusExchange } from '@urql/exchange-refocus'; 176 177const client = new Client({ 178 url: 'some-url', 179 exchanges: [refocusExchange(), cacheExchange, fetchExchange], 180}); 181``` 182 183That's all we need to do to react to these patterns.