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;