Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/react-deferred-fragments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'urql': minor
---

Add a beta React `useFragment` hook that masks fragment data and suspends while streamed `@defer` selections are incomplete. Deferred tracking happens in `@urql/core`'s `Client`, so nested Suspense boundaries resolve directly from later query results during client and server rendering, and outside of Suspense the hook updates through `makeFragmentSource` when deferred patches arrive.
90 changes: 76 additions & 14 deletions docs/basics/typescript-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,10 @@ GraphQL Code Generator generates type helpers to type your component props based
Again, here is an example with the React bindings:

```tsx
import { FragmentType, useFragment } from './gql/fragment-masking';
import { useFragment } from 'urql';
import type { FragmentType } from './gql/fragment-masking';
import { graphql } from '../src/gql';

// again, we use the generated `graphql()` function to write GraphQL documents 👀
export const FilmFragment = graphql(/* GraphQL */ `
fragment FilmItem on Film {
id
Expand All @@ -202,26 +202,88 @@ export const FilmFragment = graphql(/* GraphQL */ `
}
`);

const Film = (props: {
// `film` property has the correct type 🎉
film: FragmentType<typeof FilmFragment>;
}) => {
// `film` is of type `FilmFragment`, with no extraneous properties ⚡️
const film = useFragment(FilmFragment, props.film);
return (
const Film = (props: { film: FragmentType<typeof FilmFragment> }) => {
const { data: film } = useFragment({
fragment: FilmFragment,
data: props.film,
});

return film ? (
<div>
<h3>{film.title}</h3>
<p>{film.releaseDate}</p>
</div>
);
) : null;
};

export default Film;
```

_Examples with Vue are available [in the GraphQL Code Generator repository](https://github.com/dotansimha/graphql-code-generator/tree/master/examples/vue/urql)_.
The `FragmentType` reference and the fragment's result type are intentionally
separate. `useFragment` accepts the generated reference as its input and infers
its returned `data` from `FilmFragment`. This also allows an incremental
fragment reference to be passed before an `@defer` patch has arrived; with
Suspense enabled, the hook waits for that patch.

GraphQL Code Generator calls its generated unmasking helper `useFragment` by
default, but that helper isn't a React hook. To avoid a naming collision, name
it `readFragment` (or `getFragmentData`) in your Codegen configuration:

```ts
presetConfig: {
fragmentMasking: {
unmaskFunctionName: 'readFragment',
},
},
```

The generated `readFragment(Fragment, data)` helper may still be used before
calling urql's hook for non-deferred data. It is not required: passing the
fragment reference directly is preferred for `@defer`, since Codegen's
incremental reference is not considered fully readable until its patch arrives.

For a deferred fragment, type the component input from the parent query field so
its incremental state is retained:

```tsx
const Film = (props: { film: NonNullable<FilmsQuery['film']> }) => {
const { data: film } = useFragment({
fragment: FilmFragment,
data: props.film,
});
// ...
};
```

### Using gql.tada fragment references

gql.tada's opaque `FragmentOf` references are also accepted directly:

```tsx
import { useFragment } from 'urql';
import { graphql, type FragmentOf } from 'gql.tada';

const FilmFragment = graphql(`
fragment FilmItem on Film {
id
title
releaseDate
}
`);

const Film = (props: { film: FragmentOf<typeof FilmFragment> }) => {
const { data: film } = useFragment({
fragment: FilmFragment,
data: props.film,
});
return film ? <h3>{film.title}</h3> : null;
};
```

You will notice that our `<Film>` component leverages 2 imports from our generated code (from `../src/gql`): the `FragmentType<T>` type helper and the `useFragment()` function.
You may equivalently pass
`readFragment(FilmFragment, props.film)` as `data`. Both gql.tada and GraphQL
Code Generator's readers preserve `null` and `undefined`, which `useFragment`
also returns unchanged. For deferred fields, pass the opaque/incremental
reference directly so the hook can suspend until the streamed patch arrives.

- we use `FragmentType<typeof FilmFragment>` to get the corresponding Fragment TypeScript type
- later on, we use `useFragment()` to retrieve the properly film property
_Examples with Vue are available [in the GraphQL Code Generator repository](https://github.com/dotansimha/graphql-code-generator/tree/master/examples/vue/urql)._
1 change: 1 addition & 0 deletions packages/react-urql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"react": ">= 16.8.0"
},
"dependencies": {
"@0no-co/graphql.web": "^1.0.13",
"@urql/core": "workspace:^6.0.3",
"wonka": "^6.3.2"
},
Expand Down
64 changes: 64 additions & 0 deletions packages/react-urql/src/hooks/cache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { pipe, subscribe } from 'wonka';
import type { FragmentDefinitionNode } from '@0no-co/graphql.web';
import type { Client, OperationResult } from '@urql/core';

type CacheEntry = OperationResult | Promise<unknown> | undefined;
Expand All @@ -10,7 +11,21 @@ interface Cache {
dispose(key: number): void;
}

/** A pending suspense {@link Promise} that the `useFragment` hook throws.
*
* @internal
*/
export type FragmentPromise = Promise<unknown> & {
_resolve: () => void;
};

type FragmentCache = WeakMap<
FragmentDefinitionNode,
WeakMap<object, FragmentPromise>
>;

interface ClientWithCache extends Client {
_fragments?: FragmentCache;
_react?: Cache;
}

Expand Down Expand Up @@ -51,3 +66,52 @@ export const getCacheForClient = (client: Client): Cache => {

return (client as ClientWithCache)._react!;
};

/** Returns a pending fragment promise for this exact fragment and data object.
*
* @remarks
* Weak keys keep sibling objects and named fragments independent and allow
* abandoned suspended renders to be garbage-collected without an effect.
*
* @internal
*/
export const getFragmentPromise = (
client: Client,
fragment: FragmentDefinitionNode,
data: object
): FragmentPromise | undefined => {
const cache = (client as ClientWithCache)._fragments;
const entries = cache && cache.get(fragment);
return entries && entries.get(data);
};

/** Stores a pending promise for this exact fragment and data object.
*
* @internal
*/
export const setFragmentPromise = (
client: Client,
fragment: FragmentDefinitionNode,
data: object,
promise: FragmentPromise
): void => {
const target = client as ClientWithCache;
const cache = target._fragments || (target._fragments = new WeakMap());
let entries = cache.get(fragment);
if (!entries) cache.set(fragment, (entries = new WeakMap()));
entries.set(data, promise);
};

/** Deletes a settled fragment promise.
*
* @internal
*/
export const deleteFragmentPromise = (
client: Client,
fragment: FragmentDefinitionNode,
data: object
): void => {
const cache = (client as ClientWithCache)._fragments;
const entries = cache && cache.get(fragment);
if (entries) entries.delete(data);
};
1 change: 1 addition & 0 deletions packages/react-urql/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useFragment';
export * from './useMutation';
export * from './useQuery';
export * from './useSubscription';
Loading
Loading