Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 5.0 kB view raw
1import React, { useCallback } from 'react'; 2import { gql, useQuery } from 'urql'; 3 4// We define a fragment, just to define the data 5// that our item component will use in the results list 6const packageFragment = gql` 7 fragment SearchPackage on Package { 8 id 9 name 10 latest: version(selector: "latest") { 11 version 12 } 13 } 14`; 15 16// The main query fetches the first page of results and gets our `PageInfo` 17// This tells us whether more pages are present which we can query. 18const rootQuery = gql` 19 query SearchRoot($searchTerm: String!, $resultsPerPage: Int!) { 20 search(query: $searchTerm, first: $resultsPerPage) { 21 edges { 22 cursor 23 node { 24 ...SearchPackage 25 } 26 } 27 pageInfo { 28 hasNextPage 29 endCursor 30 } 31 } 32 } 33 34 ${packageFragment} 35`; 36 37// We split the next pages we load into a separate query. In this example code, 38// both queries could be the same, but we keep them separate for educational 39// purposes. 40// In a real app, your "root query" would often fetch more data than the search page query. 41const pageQuery = gql` 42 query SearchPage( 43 $searchTerm: String! 44 $resultsPerPage: Int! 45 $afterCursor: String! 46 ) { 47 search(query: $searchTerm, first: $resultsPerPage, after: $afterCursor) { 48 edges { 49 cursor 50 node { 51 ...SearchPackage 52 } 53 } 54 pageInfo { 55 hasNextPage 56 endCursor 57 } 58 } 59 } 60 61 ${packageFragment} 62`; 63 64// This is the <SearchRoot> component that we render in `./App.jsx`. 65// It accepts our variables as props. 66const SearchRoot = ({ searchTerm = 'urql', resultsPerPage = 10 }) => { 67 const [rootResult] = useQuery({ 68 query: rootQuery, 69 variables: { 70 searchTerm, 71 resultsPerPage, 72 }, 73 }); 74 75 if (rootResult.fetching) { 76 return <em>Loading...</em>; 77 } 78 79 // Here, we render the results as a list into a fragment, and if `hasNextPage` 80 // is truthy, we immediately render <SearchPage> for the next page. 81 const connection = rootResult.data?.search; 82 return ( 83 <> 84 {connection?.edges?.length === 0 ? <strong>No Results</strong> : null} 85 86 {connection?.edges.map(edge => ( 87 <Package key={edge.cursor} node={edge.node} /> 88 ))} 89 90 {/* The <SearchPage> component receives the same props, plus the `afterCursor` for its variables */} 91 {connection?.pageInfo.hasNextPage ? ( 92 <SearchPage 93 searchTerm={searchTerm} 94 resultsPerPage={resultsPerPage} 95 afterCursor={connection.pageInfo.endCursor} 96 /> 97 ) : rootResult.fetching ? ( 98 <em>Loading...</em> 99 ) : null} 100 </> 101 ); 102}; 103 104// The <SearchPage> is rendered for each page of results, except for the root query. 105// It renders *itself* recursively, for the next page of results. 106const SearchPage = ({ searchTerm, resultsPerPage, afterCursor }) => { 107 // Each <SearchPage> fetches its own page results! 108 const [pageResult, executeQuery] = useQuery({ 109 query: pageQuery, 110 // Initially, we *only* want to display results if, they're cached 111 requestPolicy: 'cache-only', 112 // We don't want to run the query if we don't have a cursor (in this example, this will never happen) 113 pause: !afterCursor, 114 variables: { 115 searchTerm, 116 resultsPerPage, 117 afterCursor, 118 }, 119 }); 120 121 // We only load more results, by allowing the query to make a network request, if 122 // a button has pressed. 123 // In your app, you may want to do this automatically if the user can see the end of 124 // your list, e.g. via an IntersectionObserver. 125 const onLoadMore = useCallback(() => { 126 // This tells the query above to execute and instead of `cache-only`, which forbids 127 // network requests, we now allow them. 128 executeQuery({ requestPolicy: 'cache-first' }); 129 }, [executeQuery]); 130 131 if (pageResult.fetching) { 132 return <em>Loading...</em>; 133 } 134 135 const connection = pageResult.data?.search; 136 return ( 137 <> 138 {/* If our query has nodes, we render them here. The page renders its own results */} 139 {connection?.edges.map(edge => ( 140 <Package key={edge.cursor} node={edge.node} /> 141 ))} 142 143 {/* If we have a next page, we now render it recursively! */} 144 {/* As before, the next <SearchPage> will not fetch immediately, but only query from cache */} 145 {connection?.pageInfo.hasNextPage ? ( 146 <SearchPage 147 searchTerm={searchTerm} 148 resultsPerPage={resultsPerPage} 149 afterCursor={connection.pageInfo.endCursor} 150 /> 151 ) : pageResult.fetching ? ( 152 <em>Loading...</em> 153 ) : null} 154 155 {!connection && !pageResult.fetching ? ( 156 <button type="button" onClick={onLoadMore}> 157 Load more 158 </button> 159 ) : null} 160 </> 161 ); 162}; 163 164// This is the component that then renders each result item 165const Package = ({ node }) => ( 166 <section> 167 <strong>{node.name}</strong> 168 <em>@{node.latest.version}</em> 169 </section> 170); 171 172export default SearchRoot;