-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Document new React APIs in 3.9 - Part 1 of 2 #11512
Changes from 19 commits
c4f727c
08eb80e
8a427f0
fe9674a
fefd6a4
ebe8700
908a1e7
28ab24d
431e55c
4434d90
2499789
bb96e55
e5d6c6f
a7cf933
23c54cc
3ab6262
e2e51e1
173c1a4
4c6877d
b3c64bb
958d6ba
7be17c1
4cdcb5b
b1e423a
d7db9a9
76db390
efa6f23
8577574
53a08ee
974a59f
2c556f1
5a40df9
892db6a
a754c6c
41c794b
97bb5ba
ea04c9e
9df38c6
0238b0d
974133b
4c541ac
0f9f34a
87ed45a
aa7c5d9
4a4b1ba
fdcd394
864ff7f
3503883
f701bb6
7343d6c
680714d
d457cad
f3d9fde
f7c1de1
08c845d
4176bfe
b1c44cf
c9a1c24
d6ff311
5966ad2
b76cd25
3207653
a135979
820742a
c236219
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -433,6 +433,194 @@ When the network request for `GET_DOG_QUERY` completes, the `Dog` component unsu | |
|
||
The `useBackgroundQuery` hook used in a parent component is responsible for kicking off fetches, but doesn't deal with reading or rendering data. This is delegated to the `useReadQuery` hook used in a child component. This separation of concerns provides a nice performance benefit because cache updates are observed by `useReadQuery` and re-render only the child component. You may find this as a useful tool to optimize your component structure to avoid unnecessarily re-rendering parent components when cache data changes. | ||
|
||
<MinVersion version="3.9.0"> | ||
|
||
### Querying in response to user interaction | ||
|
||
</MinVersion> | ||
|
||
`useSuspenseQuery` and `useBackgroundQuery` are useful hooks that will begin loading data as soon as the hook is mounted. But what happens when you'd like to start loading your query in response to a user interaction? With `useSuspenseQuery` and `useBackgroundQuery`, you'd need to update React state in response to the user interaction in order to re-render your component. | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Starting with Apollo Client `3.9.0`, you can use `useLoadableQuery` to start loading data in response to user interaction. This is not only more ergonomic, but starts loading the query immediately, providing a nice performance benefit. | ||
phryneas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
`useLoadableQuery` returns a function that when called will begin fetching the query with the provided variables. Like `useBackgroundQuery`, you get access to a `queryRef` that is passed to `useReadQuery` to read the data in a child component. When the child component renders before the query has finished loading, the child component suspends. | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Let's update our example to start loading the dog's details as a result of selecting from a dropdown. | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```tsx {3,8,13,22,29} | ||
import { | ||
// ... | ||
useLoadableQuery | ||
} from '@apollo/client'; | ||
|
||
function App() { | ||
const { data } = useSuspenseQuery(GET_DOGS_QUERY); | ||
const [queryRef, loadDog] = useLoadableQuery(GET_DOG_QUERY) | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return ( | ||
<> | ||
<select | ||
onChange={(e) => loadDog({ id: e.target.value })} | ||
> | ||
{data.dogs.map(({ id, name }) => ( | ||
<option key={id} value={id}> | ||
{name} | ||
</option> | ||
))} | ||
</select> | ||
<Suspense fallback={<div>Loading...</div>}> | ||
{queryRef && <Dog queryRef={queryRef} />} | ||
</Suspense> | ||
</> | ||
); | ||
} | ||
|
||
function Dog({ queryRef }) { | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { data } = useReadQuery(queryRef) | ||
|
||
return ( | ||
<> | ||
<div>Name: {data.dog.name}</div> | ||
<div>Breed: {data.dog.breed}</div> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
> It's important to note that the `queryRef` is `null` until the query loading function is called for the first time. You should conditionally render the child component when the query has not yet been loaded. | ||
|
||
We begin fetching our `GET_DOG_QUERY` as soon as a new dog is selected rather than waiting for our `Dog` component to re-render. When the `Dog` component renders, it reads data from the `GET_DOG_QUERY`, and if not ready, will suspend. As a result of this change, we've also removed the need to track the selected dog ID as part of React state! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Again, I'm not sure if this should be the main selling angle here - that render is probably instant, and it will happen either way, so a data fetch would at best happen a handful of milliseconds sooner. I'd really focus more on the data flow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Take 2 d7db9a9 |
||
|
||
<MinVersion version="3.9.0"> | ||
|
||
### Initiating queries outside React | ||
|
||
</MinVersion> | ||
|
||
Starting with Apollo Client `3.9.0`, it is possible to start loading queries outside React. This avoids the need to wait for React to render your components before the query begins loading, providing a nice performance benefit. | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<ExperimentalFeature> | ||
|
||
This API is experimental in `3.9.0` and may change in `3.10.0`. We are specifically looking for user feedback on the API design choices. The underlying behavior is otherwise fully implemented. If you'd like to provide feedback for this feature before it is stabilized in `3.10.0`, please [open an issue](https://github.com/apollographql/apollo-client/issues/new). | ||
|
||
</ExperimentalFeature> | ||
|
||
To begin loading data immediately, you first need to create a preload function with `createQueryPreloader`. `createQueryPreloader` is given an `ApolloClient` instance as an argument and returns a function that when called, will begin fetching the query. By making `createQueryPreloader` a [curried](https://en.wikipedia.org/wiki/Currying) function, this makes it easy to export your preload function alongside any `ApolloClient` instance you create. | ||
|
||
The preload function returns a `queryRef` that can then be passed to `useReadQuery` to read the query data and suspend the component while loading. `useReadQuery` will also ensure that your component containing the preloaded query will be kept up-to-date with any cache updates for that query. | ||
|
||
Let's update our example to start loading the `GET_DOGS_QUERY` before our app is rendered. | ||
|
||
```tsx {3,6-7,10,33,35} | ||
import { | ||
// ... | ||
createQueryPreloader | ||
} from '@apollo/client'; | ||
|
||
const preloadQuery = createQueryPreloader(client); | ||
const preloadedQueryRef = preloadQuery(GET_DOGS_QUERY); | ||
|
||
function App() { | ||
const { data } = useReadQuery(preloadedQueryRef); | ||
const [queryRef, loadDog] = useLoadableQuery(GET_DOG_QUERY) | ||
|
||
return ( | ||
<> | ||
<select | ||
onChange={(e) => loadDog({ id: e.target.value })} | ||
> | ||
{data.dogs.map(({ id, name }) => ( | ||
<option key={id} value={id}>{name}</option> | ||
))} | ||
</select> | ||
<Suspense fallback={<div>Loading...</div>}> | ||
<Dog queryRef={queryRef} /> | ||
</Suspense> | ||
</> | ||
); | ||
} | ||
|
||
const root = createRoot(document.getElementById('app')); | ||
|
||
root.render( | ||
<ApolloProvider client={client}> | ||
<Suspense fallback={<div>Loading...</div>}> | ||
<App /> | ||
</Suspense> | ||
</ApolloProvider> | ||
); | ||
``` | ||
|
||
We begin loading data as soon as our `preloadQuery` function is called. We no longer need to wait for React to render our `<App />` component before our `GET_DOGS_QUERY` begins loading, but still get the benefit of suspense! | ||
|
||
#### Usage with data loading routers | ||
|
||
Popular routers such as [React Router](https://reactrouter.com/en/main) and [TanStack Router](https://tanstack.com/router) include APIs to start loading data before the route component renders (e.g. such as the [`loader` function](https://reactrouter.com/en/main/route/route#loader) from React Router). These APIs have the benefit that they can begin loading data in parallel without the need to wait for the route components to render. This is especially useful for nested routes where a parent route might otherwise suspend and create request waterfalls for child routes. | ||
|
||
The `preloadQuery` API pairs nicely with these router APIs as it lets you take advantage of the optimizations these routers provide without sacrificing the ability to respond to cache updates in your route components. | ||
|
||
Let's update our example using React Router's `loader` function to begin loading data when we transition to our route. | ||
|
||
```ts {4,8-9} | ||
import { useLoaderData } from 'react-router-dom'; | ||
|
||
export function loader() { | ||
return preloadQuery(GET_DOGS_QUERY); | ||
} | ||
|
||
export function RouteComponent() { | ||
const queryRef = useLoaderData(); | ||
const { data } = useReadQuery(queryRef); | ||
|
||
return ( | ||
// ... | ||
); | ||
} | ||
``` | ||
|
||
> The `loader` function is available in React Router versions 6.4 and above. | ||
|
||
We load our query as soon as React Router calls our `loader` function. This starts fetching before our route component is rendered. When our route component renders, we can get access to the `queryRef` created by our `preloadQuery` function via the `useLoaderData` hook, which is then passed to `useReadQuery` to read the data. We get the benefit of loading our data early in the routing lifecycle, while our route component has the ability to update itself when the cache is updated! | ||
|
||
<Note> | ||
|
||
The `preloadQuery` API only works with client-side routing. The `queryRef` returned from `preloadQuery` is not serializable across the wire and as such, will not work with routers that fetch on the server such as [Remix](https://remix.run/). | ||
|
||
</Note> | ||
|
||
#### Preventing route transitions until the query is loaded | ||
|
||
By default, `preloadQuery` works similar to a [deferred loader](https://reactrouter.com/en/main/guides/deferred) in that routes will transition immediately and suspend with `useReadQuery` until the query is loaded. You may instead want to prevent the route from transitioning until the data is fully loaded. Use the `toPromise` method on the `queryRef` to get a promise that resolves when the query is loaded. This promise resolves with the `queryRef` itself, making it easy to use with hooks such as `useLoaderData`. | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```ts | ||
export async function loader() { | ||
const queryRef = await preloadQuery(GET_DOGS_QUERY).toPromise(); | ||
|
||
return queryRef; | ||
} | ||
|
||
// You may also return the promise directly from loader. | ||
// This is equivalent to the above. | ||
export async function loader() { | ||
return preloadQuery(GET_DOGS_QUERY).toPromise(); | ||
} | ||
|
||
export function RouteComponent() { | ||
const queryRef = useLoaderData(); | ||
const { data } = useReadQuery(queryRef); | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
This instructs React Router to wait for the query to finish loading before the route transitions. When the route transitions after the promise resolves, the data will be rendered immediately without the need to show a loading fallback in the route component. | ||
|
||
#### Why prevent access to `data` in `toPromise`? | ||
|
||
You may be wondering why we resolve `toPromise` with the `queryRef` itself, rather than the data loaded from the query. This is because we want to encourage you to leverage `useReadQuery` to avoid missing out on cache updates for your query. If `data` were available, it would be tempting to consume it in your `loader` functions and expose it to your route components. Doing so means missing out on cache updates. | ||
|
||
If you need access to raw query data in your `loader` functions, use [`client.query()`](../api/core/ApolloClient#query) directly. | ||
alessbell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Refetching and pagination | ||
|
||
Apollo's Suspense data fetching hooks return functions for refetching query data via the `refetch` function, and fetching additional pages of data via the `fetchMore` function. | ||
|
@@ -513,6 +701,133 @@ function Breeds({ queryRef, isPending }: BreedsProps) { | |
|
||
In this example, our `App` component renders a `Dog` component that fetches a single dog's record via `useSuspenseQuery`. When React attempts to render `Dog` for the first time, the cache can't fulfill the request for the `GetDog` query, so `useSuspenseQuery` initiates a network request. `Dog` suspends while the network request is pending, triggering the nearest `Suspense` boundary _above_ the suspended component in `App` which renders our "Loading..." fallback. Once the network request is complete, `Dog` renders with the newly cached `name` for the dog whose `id="3"`: Mozzarella the Corgi. | ||
|
||
#### Usage with query preloading | ||
|
||
When loading queries [outside React](#initiating-queries-outside-react), the `preloadQuery` function returns a `queryRef` with no access to `refetch` or `fetchMore` functions. This presents a challenge if you need to refresh or paginate on the data returned by the query ref. | ||
|
||
You can gain access to `refetch` and `fetchMore` functions by using the `useQueryRefHandlers` hook. This hook integrates with React transitions, giving you the ability to refetch or paginate without showing the loading fallback. | ||
|
||
Let's update our example to preload our `GET_BREEDS_QUERY` outside React and utilize `useQueryRefHandlers` to refetch our query. | ||
|
||
```tsx {4,7-8,12} | ||
// ... | ||
import { | ||
// ... | ||
useQueryRefHandlers, | ||
} from "@apollo/client"; | ||
|
||
const preloadQuery = createQueryPreloader(client); | ||
const queryRef = preloadQuery(GET_BREEDS_QUERY); | ||
|
||
function App() { | ||
const [isPending, startTransition] = useTransition(); | ||
const { refetch } = useQueryRefHandlers(queryRef); | ||
|
||
function handleRefetch() { | ||
startTransition(() => { | ||
refetch(); | ||
}); | ||
}; | ||
|
||
return ( | ||
<Suspense fallback={<div>Loading...</div>}> | ||
<Dog | ||
id="3" | ||
isPending={isPending} | ||
onRefetch={handleRefetch} | ||
/> | ||
</Suspense> | ||
); | ||
} | ||
|
||
function Dog({ | ||
id, | ||
isPending, | ||
onRefetch, | ||
}: DogProps) { | ||
const { data } = useSuspenseQuery(GET_DOG_QUERY, { | ||
variables: { id }, | ||
}); | ||
|
||
return ( | ||
<> | ||
Name: {data.dog.name} | ||
<Suspense fallback={<div>Loading breeds...</div>}> | ||
<Breeds isPending={isPending} /> | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</Suspense> | ||
<button onClick={onRefetch}>Refetch!</button> | ||
</> | ||
); | ||
} | ||
|
||
function Breeds({ isPending }: BreedsProps) { | ||
const { data } = useReadQuery(queryRef); | ||
|
||
return data.breeds.map(({ characteristics }) => | ||
characteristics.map((characteristic) => ( | ||
<div | ||
style={{ opacity: isPending ? 0.5 : 1 }} | ||
key={characteristic} | ||
> | ||
{characteristic} | ||
</div> | ||
)) | ||
); | ||
} | ||
``` | ||
|
||
We begin loading `GET_BREEDS_QUERY` outside of React with the `preloadQuery` function. We use the `useQueryRefHandlers` hook to get access to the `refetch` function which lets us refetch the query when the button is clicked. | ||
|
||
#### Accessing query ref handlers produced from other hooks | ||
jerelmiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
`useQueryRefHandlers` is not limited to the `preloadQuery` API and can be used with any hook that produces a `queryRef` such as `useBackgroundQuery` or `useLoadableQuery`. This can be useful in situations where you may need access to the `refetch` and `fetchMore` functions in components where the `queryRef` was passed through deeply. | ||
|
||
Let's update our last example back to `useBackgroundQuery` and get access to `refetch` from `useQueryRefHandlers` in our `Dog` component without passing the `refetch` function from the parent. | ||
|
||
```tsx {12,16,18-22,30} | ||
function App() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to throw the rest of these in the codesandbox too 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes thank you! Was on my todo, but hadn't got to it yet! |
||
const [queryRef] = useBackgroundQuery(GET_BREEDS_QUERY); | ||
|
||
return ( | ||
<Suspense fallback={<div>Loading...</div>}> | ||
<Dog id="3" queryRef={queryRef} /> | ||
</Suspense> | ||
); | ||
} | ||
|
||
function Dog({ id, queryRef }: DogProps) { | ||
const [isPending, startTransition] = useTransition(); | ||
const { data } = useSuspenseQuery(GET_DOG_QUERY, { | ||
variables: { id }, | ||
}); | ||
const { refetch } = useQueryRefHandlers(queryRef); | ||
|
||
function handleRefetch() { | ||
startTransition(() => { | ||
refetch(); | ||
}); | ||
}; | ||
|
||
return ( | ||
<> | ||
Name: {data.dog.name} | ||
<Suspense fallback={<div>Loading breeds...</div>}> | ||
<Breeds queryRef={queryRef} isPending={isPending} /> | ||
</Suspense> | ||
<button onClick={handleRefetch}>Refetch!</button> | ||
</> | ||
); | ||
} | ||
|
||
// ... | ||
``` | ||
|
||
<Note> | ||
|
||
Using the handlers returned from `useQueryRefHandlers` does not prevent you from using the handlers produced by query ref hooks. You can use the handlers in both locations, with or without React transitions to produce the desired result. | ||
|
||
</Note> | ||
|
||
## Distinguishing between queries with `queryKey` | ||
|
||
Apollo Client uses the combination of `query` and `variables` to uniquely identify each query when using Apollo's Suspense data fetching hooks. | ||
|
@@ -576,6 +891,14 @@ More details on `useSuspenseQuery`'s API can be found in [its API docs](../api/r | |
|
||
More details on `useBackgroundQuery`'s API can be found in [its API docs](../api/react/hooks/#usebackgroundquery). | ||
|
||
## useLoadableQuery API | ||
|
||
More details on `useLoadableQuery`'s API can be found in [its API docs](../api/react/hooks/#useloadablequery). | ||
|
||
## useQueryRefHandlers API | ||
|
||
More details on `useQueryRefHandlers`'s API can be found in [its API docs](../api/react/hooks/#usequeryrefhandlers). | ||
|
||
## useReadQuery API | ||
|
||
More details on `useReadQuery`'s API can be found in [its API docs](../api/react/hooks/#usereadquery). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another point of feedback: Does this section include enough information?
Some other things we could talk about if we want to provide additional info:
variables
option and instead variables are passed to theloadQuery
function.reset
function that when called, sets thequeryRef
back tonull
This section is also provided right before pagination/refetching, which uses
useBackgroundQuery
as the example. Should this be moved afterward? Feedback welcome!