Skip to content
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

Feature: useSuspenseFragment #12066

Open
wants to merge 43 commits into
base: release-3.13
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5ff3a65
Add way to cache fragment promises
jerelmiller Sep 17, 2024
8ecae2e
Add some code to handle suspenseful useFragment
jerelmiller Sep 18, 2024
5313228
Add handling of promise state in FragmentReference
jerelmiller Sep 18, 2024
c07aaa4
Add some basic tests for useSuspenseFragment
jerelmiller Sep 18, 2024
901c15f
Add exports for useSuspenseFragment
jerelmiller Sep 18, 2024
f930f45
Remove unused code
jerelmiller Sep 18, 2024
c54d8fb
Update exports snapshot
jerelmiller Sep 18, 2024
5fc5aea
Wrap useSuspenseFragment hook for streaming package
jerelmiller Sep 18, 2024
b71dffe
Handle different cache ids in useSuspenseFragment
jerelmiller Sep 18, 2024
62d375d
Update tests to use render stream library
jerelmiller Dec 17, 2024
941c050
Add useSuspenseFragment to list of ignored React 17 tests
jerelmiller Dec 17, 2024
f9b843c
Fix types to work with data masking
jerelmiller Dec 17, 2024
7c588e1
Add test that ensures error is thrown when passing non-fragment to us…
jerelmiller Dec 17, 2024
e5b536d
Inline the cache key
jerelmiller Dec 17, 2024
5c3d736
Add test for complete result on first render
jerelmiller Dec 17, 2024
7a07936
Create observable in FragmentReference
jerelmiller Dec 17, 2024
f42452d
Handle case where cache data is already written
jerelmiller Dec 17, 2024
a18fff1
Fix typo
jerelmiller Dec 17, 2024
5584741
Add test to ensure cache updates are received after initial result
jerelmiller Dec 17, 2024
48ff08e
Add test to ensure data can be overwritten
jerelmiller Dec 17, 2024
dd2d5ce
Add test to ensure client is provided
jerelmiller Dec 17, 2024
5d9c2e1
Add test to ensure changing from values suspends correctly
jerelmiller Dec 17, 2024
febce9a
Add test for changing from with data already written to cache
jerelmiller Dec 17, 2024
316d0cb
Add test to check @nonreactive
jerelmiller Dec 17, 2024
5e18984
Add test for @nonreactive with nested fragment
jerelmiller Dec 17, 2024
8842eff
Add failing test for suspending when not passing key fields to fragment
jerelmiller Dec 17, 2024
23d6b3e
Use deep equality in FragmentReference
jerelmiller Dec 17, 2024
dccaaac
Add tests for data masking
jerelmiller Dec 17, 2024
4cbe24d
Add test to check for cache updates to masked fields
jerelmiller Dec 17, 2024
5248b63
Add support for `null` value as `from` in `useSuspenseFragment`
jerelmiller Dec 18, 2024
45d0741
Add test to ensure changing from null works as expected
jerelmiller Dec 18, 2024
39f871d
Remove unneeded type cast
jerelmiller Dec 18, 2024
ce33bc3
Add test to ensure useSuspenseFragment suspends when switching from null
jerelmiller Dec 18, 2024
a672701
Add test to ensure switching from non-null to null works as expected
jerelmiller Dec 18, 2024
91beda7
Update api report and size limits
jerelmiller Dec 18, 2024
5f09a50
Add changeset
jerelmiller Dec 18, 2024
6a4132d
Add tests to ensure suspense fragment is torn down
jerelmiller Dec 19, 2024
accc6c0
Add support for autoDisposeTimeoutMs in useSuspenseFragment
jerelmiller Dec 19, 2024
1946b9c
Clean up Prettier, Size-limit, and Api-Extractor
jerelmiller Dec 19, 2024
972805d
Default TVariables to OperationVariables
jerelmiller Dec 20, 2024
b727b1d
Add explicit generic arguments to tests
jerelmiller Dec 20, 2024
bbeb330
Allow for FragmentType
jerelmiller Dec 20, 2024
fefec82
Clean up Prettier, Size-limit, and Api-Extractor
jerelmiller Dec 20, 2024
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
34 changes: 34 additions & 0 deletions .api-reports/api-report-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2416,6 +2416,40 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
variables?: TVariables | undefined;
};

// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentOptions" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {};
}): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: null;
}): UseSuspenseFragmentResult<null>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {} | null;
}): UseSuspenseFragmentResult<TData | null>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables>): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
interface UseSuspenseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoInfer<TData>, NoInfer<TVars>>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit<Cache_2.ReadFragmentOptions<TData, TVars>, "id" | "variables" | "returnPartialData"> {
client?: ApolloClient<any>;
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}

// @public (undocumented)
export type UseSuspenseFragmentResult<TData> = {
data: MaybeMasked<TData>;
};

// @public (undocumented)
export function useSuspenseQuery<TData, TVariables extends OperationVariables, TOptions extends Omit<SuspenseQueryHookOptions<TData>, "variables">>(query: DocumentNode | TypedDocumentNode<TData, TVariables>, options?: SuspenseQueryHookOptions<NoInfer_2<TData>, NoInfer_2<TVariables>> & TOptions): UseSuspenseQueryResult<TOptions["errorPolicy"] extends "ignore" | "all" ? TOptions["returnPartialData"] extends true ? DeepPartial<TData> | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? TOptions["skip"] extends boolean ? DeepPartial<TData> | undefined : DeepPartial<TData> : TOptions["skip"] extends boolean ? TData | undefined : TData, TVariables>;

Expand Down
34 changes: 34 additions & 0 deletions .api-reports/api-report-react_hooks.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2249,6 +2249,40 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
variables?: TVariables | undefined;
};

// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentOptions" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {};
}): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: null;
}): UseSuspenseFragmentResult<null>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {} | null;
}): UseSuspenseFragmentResult<TData | null>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables>): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
interface UseSuspenseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoInfer<TData>, NoInfer<TVars>>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit<Cache_2.ReadFragmentOptions<TData, TVars>, "id" | "variables" | "returnPartialData"> {
client?: ApolloClient<any>;
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}

// @public (undocumented)
export type UseSuspenseFragmentResult<TData> = {
data: MaybeMasked<TData>;
};

// Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
Expand Down
101 changes: 99 additions & 2 deletions .api-reports/api-report-react_internal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,19 @@ interface FieldSpecifier {
variables?: Record<string, any>;
}

// @public (undocumented)
type FragmentCacheKey = [
cacheId: string,
fragment: DocumentNode,
stringifiedVariables: string
];

// @public (undocumented)
interface FragmentKey {
// (undocumented)
__fragmentKey?: string;
}

// @public
interface FragmentMap {
// (undocumented)
Expand All @@ -822,6 +835,43 @@ interface FragmentMap {
// @public (undocumented)
type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean;

// @public (undocumented)
class FragmentReference<TData = unknown, TVariables = Record<string, unknown>> {
// Warning: (ae-forgotten-export) The symbol "FragmentReferenceOptions" needs to be exported by the entry point index.d.ts
constructor(client: ApolloClient<any>, watchFragmentOptions: WatchFragmentOptions<TData, TVariables> & {
from: string;
}, options: FragmentReferenceOptions);
// Warning: (ae-forgotten-export) The symbol "FragmentKey" needs to be exported by the entry point index.d.ts
//
// (undocumented)
readonly key: FragmentKey;
// Warning: (ae-forgotten-export) The symbol "Listener_2" needs to be exported by the entry point index.d.ts
//
// (undocumented)
listen(listener: Listener_2<MaybeMasked<TData>>): () => void;
// (undocumented)
readonly observable: Observable<WatchFragmentResult<TData>>;
// Warning: (ae-forgotten-export) The symbol "FragmentRefPromise" needs to be exported by the entry point index.d.ts
//
// (undocumented)
promise: FragmentRefPromise<MaybeMasked<TData>>;
// (undocumented)
retain(): () => void;
}

// @public (undocumented)
interface FragmentReferenceOptions {
// (undocumented)
autoDisposeTimeoutMs?: number;
// (undocumented)
onDispose?: () => void;
}

// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
type FragmentRefPromise<TData> = PromiseWithState<TData>;

// @public (undocumented)
type FragmentType<TData> = [
TData
Expand Down Expand Up @@ -1042,6 +1092,9 @@ type IsStrictlyAny<T> = UnionToIntersection<UnionForAny<T>> extends never ? true
// @public (undocumented)
type Listener<TData> = (promise: QueryRefPromise<TData>) => void;

// @public (undocumented)
type Listener_2<TData> = (promise: FragmentRefPromise<TData>) => void;

// @public (undocumented)
class LocalState<TCacheShape> {
// Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts
Expand Down Expand Up @@ -1771,8 +1824,6 @@ export interface QueryReference<TData = unknown, TVariables = unknown> extends Q
toPromise?: unknown;
}

// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
type QueryRefPromise<TData> = PromiseWithState<ApolloQueryResult<MaybeMasked<TData>>>;

Expand Down Expand Up @@ -2004,6 +2055,13 @@ class SuspenseCache {
constructor(options?: SuspenseCacheOptions);
// (undocumented)
add(cacheKey: CacheKey, queryRef: InternalQueryReference<unknown>): void;
// Warning: (ae-forgotten-export) The symbol "FragmentCacheKey" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "FragmentReference" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getFragmentRef<TData, TVariables>(cacheKey: FragmentCacheKey, client: ApolloClient<any>, options: WatchFragmentOptions<TData, TVariables> & {
from: string;
}): FragmentReference<TData, TVariables>;
// (undocumented)
getQueryRef<TData = any>(cacheKey: CacheKey, createObservable: () => ObservableQuery<TData>): InternalQueryReference<TData>;
}
Expand Down Expand Up @@ -2255,6 +2313,41 @@ interface UseReadQueryResult<TData = unknown> {
networkStatus: NetworkStatus;
}

// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentResult" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {};
}): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: null;
}): UseSuspenseFragmentResult<null>;

// @public (undocumented)
function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {} | null;
}): UseSuspenseFragmentResult<TData | null>;

// @public (undocumented)
function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables>): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
interface UseSuspenseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoInfer<TData>, NoInfer<TVars>>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit<Cache_2.ReadFragmentOptions<TData, TVars>, "id" | "variables" | "returnPartialData"> {
client?: ApolloClient<any>;
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}

// @public (undocumented)
type UseSuspenseFragmentResult<TData> = {
data: MaybeMasked<TData>;
};

// Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "UseSuspenseQueryResult" needs to be exported by the entry point index.d.ts
//
Expand Down Expand Up @@ -2382,6 +2475,10 @@ interface WrappableHooks {
//
// (undocumented)
useReadQuery: typeof useReadQuery;
// Warning: (ae-forgotten-export) The symbol "useSuspenseFragment" needs to be exported by the entry point index.d.ts
//
// (undocumented)
useSuspenseFragment: typeof useSuspenseFragment;
// Warning: (ae-forgotten-export) The symbol "useSuspenseQuery" needs to be exported by the entry point index.d.ts
//
// (undocumented)
Expand Down
34 changes: 34 additions & 0 deletions .api-reports/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3087,6 +3087,40 @@ export function useSubscription<TData = any, TVariables extends OperationVariabl
variables?: TVariables | undefined;
};

// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentOptions" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {};
}): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: null;
}): UseSuspenseFragmentResult<null>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables> & {
from: {} | null;
}): UseSuspenseFragmentResult<TData | null>;

// @public (undocumented)
export function useSuspenseFragment<TData, TVariables extends OperationVariables = OperationVariables>(options: UseSuspenseFragmentOptions<TData, TVariables>): UseSuspenseFragmentResult<TData>;

// @public (undocumented)
interface UseSuspenseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoInfer<TData>, NoInfer<TVars>>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit<Cache_2.ReadFragmentOptions<TData, TVars>, "id" | "variables" | "returnPartialData"> {
client?: ApolloClient<any>;
// (undocumented)
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string | null;
// (undocumented)
optimistic?: boolean;
}

// @public (undocumented)
export type UseSuspenseFragmentResult<TData> = {
data: MaybeMasked<TData>;
};

// @public (undocumented)
export function useSuspenseQuery<TData, TVariables extends OperationVariables, TOptions extends Omit<SuspenseQueryHookOptions<TData>, "variables">>(query: DocumentNode | TypedDocumentNode<TData, TVariables>, options?: SuspenseQueryHookOptions<NoInfer_2<TData>, NoInfer_2<TVariables>> & TOptions): UseSuspenseQueryResult<TOptions["errorPolicy"] extends "ignore" | "all" ? TOptions["returnPartialData"] extends true ? DeepPartial<TData> | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? TOptions["skip"] extends boolean ? DeepPartial<TData> | undefined : DeepPartial<TData> : TOptions["skip"] extends boolean ? TData | undefined : TData, TVariables>;

Expand Down
9 changes: 9 additions & 0 deletions .changeset/blue-comics-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@apollo/client": minor
---

Add a new `useSuspenseFragment` hook.

`useSuspenseFragment` suspends until `data` is complete. It is a drop-in
replacement for `useFragment` when you prefer to use Suspense to control the
loading state of a fragment.
2 changes: 1 addition & 1 deletion .size-limits.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"dist/apollo-client.min.cjs": 41639,
"dist/apollo-client.min.cjs": 42174,
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34381
}
1 change: 1 addition & 0 deletions config/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const react17TestFileIgnoreList = [
// We only support Suspense with React 18, so don't test suspense hooks with
// React 17
"src/testing/experimental/__tests__/createTestSchema.test.tsx",
"src/react/hooks/__tests__/useSuspenseFragment.test.tsx",
"src/react/hooks/__tests__/useSuspenseQuery.test.tsx",
"src/react/hooks/__tests__/useBackgroundQuery.test.tsx",
"src/react/hooks/__tests__/useLoadableQuery.test.tsx",
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Array [
"useReactiveVar",
"useReadQuery",
"useSubscription",
"useSuspenseFragment",
"useSuspenseQuery",
]
`;
Expand Down Expand Up @@ -293,6 +294,7 @@ Array [
"useReactiveVar",
"useReadQuery",
"useSubscription",
"useSuspenseFragment",
"useSuspenseQuery",
]
`;
Expand Down Expand Up @@ -338,6 +340,7 @@ Array [
"useReactiveVar",
"useReadQuery",
"useSubscription",
"useSuspenseFragment",
"useSuspenseQuery",
]
`;
Expand Down
Loading