From eaf62c81206ae300decbe49bab4030feed54d0d5 Mon Sep 17 00:00:00 2001 From: Martin Andert Date: Fri, 22 Nov 2019 16:45:23 +0100 Subject: [PATCH] [react-relay] Improve nullability handling of pagination hooks (#40579) * Fix nullabily of usePaginationFragment to match that of useFragment * usePaginationFragmanet: add test for the non-null case * Fix nullabily of useBlockingPaginationFragment to match that of usePaginationFragment --- .../useBlockingPaginationFragment.d.ts | 33 ++- .../useLegacyPaginationFragment.d.ts | 22 +- .../test/relay-experimental-tests.tsx | 249 +++++++++++++++++- 3 files changed, 286 insertions(+), 18 deletions(-) diff --git a/types/react-relay/lib/relay-experimental/useBlockingPaginationFragment.d.ts b/types/react-relay/lib/relay-experimental/useBlockingPaginationFragment.d.ts index d30b97e8b2..fb5fc70c4f 100644 --- a/types/react-relay/lib/relay-experimental/useBlockingPaginationFragment.d.ts +++ b/types/react-relay/lib/relay-experimental/useBlockingPaginationFragment.d.ts @@ -14,23 +14,30 @@ export interface ReturnType { export type $Call any> = Fn extends (arg: any) => infer RT ? RT : never; -export type NonNullableReturnType = (arg: T) => NonNullable; -export type NullableReturnType = (arg: T) => T[' $data'] | null; +interface KeyType { + readonly ' $data'?: unknown; +} + +type KeyReturnType = (arg: T) => NonNullable; export function useBlockingPaginationFragment< TQuery extends OperationType, - TKey extends { readonly ' $data'?: unknown | null } + TKey extends KeyType >( fragmentInput: GraphQLTaggedNode, parentFragmentRef: TKey, componentDisplayName?: string, -): ReturnType< - // tslint:disable-next-line:no-unnecessary-generics - TQuery, - TKey, - // NOTE: This $Call ensures that the type of the returned data is either: - // - nullable if the provided ref type is nullable - // - non-nullable if the provided ref type is non-nullable - // prettier-ignore - $Call & NullableReturnType> ->; +): // tslint:disable-next-line no-unnecessary-generics +ReturnType>>; + +export function useBlockingPaginationFragment< + TQuery extends OperationType, + TKey extends KeyType +>( + fragmentInput: GraphQLTaggedNode, + parentFragmentRef: TKey | null, + componentDisplayName?: string, +): // tslint:disable-next-line no-unnecessary-generics +ReturnType> | null>; + +export { }; diff --git a/types/react-relay/lib/relay-experimental/useLegacyPaginationFragment.d.ts b/types/react-relay/lib/relay-experimental/useLegacyPaginationFragment.d.ts index 0f0021eeeb..1532a043b5 100644 --- a/types/react-relay/lib/relay-experimental/useLegacyPaginationFragment.d.ts +++ b/types/react-relay/lib/relay-experimental/useLegacyPaginationFragment.d.ts @@ -15,14 +15,28 @@ export interface ReturnType { export type $Call any> = Fn extends (arg: any) => infer RT ? RT : never; -export type NonNullableReturnType = (arg: T) => NonNullable; -export type NullableReturnType = (arg: T) => T[' $data'] | null; +interface KeyType { + readonly ' $data'?: unknown; +} + +type KeyReturnType = (arg: T) => NonNullable; export function useLegacyPaginationFragment< TQuery extends OperationType, - TKey extends { readonly ' $data'?: unknown | null } + TKey extends KeyType >( fragmentInput: GraphQLTaggedNode, parentFragmentRef: TKey, ): // tslint:disable-next-line no-unnecessary-generics -ReturnType & NullableReturnType>>; +ReturnType>>; + +export function useLegacyPaginationFragment< + TQuery extends OperationType, + TKey extends KeyType +>( + fragmentInput: GraphQLTaggedNode, + parentFragmentRef: TKey | null, +): // tslint:disable-next-line no-unnecessary-generics +ReturnType> | null>; + +export {}; diff --git a/types/react-relay/test/relay-experimental-tests.tsx b/types/react-relay/test/relay-experimental-tests.tsx index b9970e2002..193b7b2a1e 100644 --- a/types/react-relay/test/relay-experimental-tests.tsx +++ b/types/react-relay/test/relay-experimental-tests.tsx @@ -12,6 +12,7 @@ import { useFragment, useRefetchableFragment, usePaginationFragment, + useBlockingPaginationFragment, } from 'react-relay/hooks'; const source = new RecordSource(); @@ -431,7 +432,7 @@ function PaginationFragment() { } interface Props { - user: FriendsListComponent_user$key; + user: FriendsListComponent_user$key | null; } return function FriendsList(props: Props) { @@ -476,3 +477,249 @@ function PaginationFragment() { ); }; } + +function PaginationFragment_WithNonNullUserProp() { + interface FriendsListPaginationQueryVariables { + count?: number; + cursor?: string; + id: string; + } + interface FriendsListPaginationQueryResponse { + readonly node: { + readonly ' $fragmentRefs': FragmentRefs<'FriendsListComponent_user'>; + }; + } + interface FriendsListPaginationQuery { + readonly response: FriendsListPaginationQueryResponse; + readonly variables: FriendsListPaginationQueryVariables; + } + + interface FriendsListComponent_user { + readonly name: string; + readonly friends: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly name: string; + readonly age: number; + }; + }>; + }; + readonly id: string; + readonly ' $refType': 'FriendsListComponent_user'; + } + type FriendsListComponent_user$data = FriendsListComponent_user; + interface FriendsListComponent_user$key { + readonly ' $data'?: FriendsListComponent_user$data; + readonly ' $fragmentRefs': FragmentRefs<'FriendsListComponent_user'>; + } + + interface Props { + user: FriendsListComponent_user$key; + } + + return function FriendsList(props: Props) { + const { + data, + loadNext, + loadPrevious, + hasNext, + hasPrevious, + isLoadingNext, + isLoadingPrevious, + refetch, // For refetching connection + } = usePaginationFragment( + graphql` + fragment FriendsListComponent_user on User @refetchable(queryName: "FriendsListPaginationQuery") { + name + friends(first: $count, after: $cursor) @connection(key: "FriendsList_user_friends") { + edges { + node { + name + age + } + } + } + } + `, + props.user, + ); + + return ( + <> +

Friends of {data.name}:

+ + {data.friends.edges.map(({ node }) => ( +
+ {node.name} - {node.age} +
+ ))} + + + + ); + }; +} + +/** + * Tests for useBlockingPaginationFragment + * see https://relay.dev/docs/en/experimental/api-reference#useblockingpaginationfragment + */ +function BlockingPaginationFragment() { + interface FriendsListPaginationQueryVariables { + count?: number; + cursor?: string; + id: string; + } + interface FriendsListPaginationQueryResponse { + readonly node: { + readonly ' $fragmentRefs': FragmentRefs<'FriendsListComponent_user'>; + }; + } + interface FriendsListPaginationQuery { + readonly response: FriendsListPaginationQueryResponse; + readonly variables: FriendsListPaginationQueryVariables; + } + + interface FriendsListComponent_user { + readonly name: string; + readonly friends: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly name: string; + readonly age: number; + }; + }>; + }; + readonly id: string; + readonly ' $refType': 'FriendsListComponent_user'; + } + type FriendsListComponent_user$data = FriendsListComponent_user; + interface FriendsListComponent_user$key { + readonly ' $data'?: FriendsListComponent_user$data; + readonly ' $fragmentRefs': FragmentRefs<'FriendsListComponent_user'>; + } + + interface Props { + user: FriendsListComponent_user$key | null; + } + + return function FriendsList(props: Props) { + const { + data, + loadNext, + loadPrevious, + hasNext, + hasPrevious, + refetch, // For refetching connection + } = useBlockingPaginationFragment( + graphql` + fragment FriendsListComponent_user on User @refetchable(queryName: "FriendsListPaginationQuery") { + name + friends(first: $count, after: $cursor) @connection(key: "FriendsList_user_friends") { + edges { + node { + name + age + } + } + } + } + `, + props.user, + ); + + return ( + <> +

Friends of {data!.name}:

+ + {data!.friends.edges.map(({ node }) => ( +
+ {node.name} - {node.age} +
+ ))} + + + + ); + }; +} + +function BlockingPaginationFragment_WithNonNullUserProp() { + interface FriendsListPaginationQueryVariables { + count?: number; + cursor?: string; + id: string; + } + interface FriendsListPaginationQueryResponse { + readonly node: { + readonly ' $fragmentRefs': FragmentRefs<'FriendsListComponent_user'>; + }; + } + interface FriendsListPaginationQuery { + readonly response: FriendsListPaginationQueryResponse; + readonly variables: FriendsListPaginationQueryVariables; + } + + interface FriendsListComponent_user { + readonly name: string; + readonly friends: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly name: string; + readonly age: number; + }; + }>; + }; + readonly id: string; + readonly ' $refType': 'FriendsListComponent_user'; + } + type FriendsListComponent_user$data = FriendsListComponent_user; + interface FriendsListComponent_user$key { + readonly ' $data'?: FriendsListComponent_user$data; + readonly ' $fragmentRefs': FragmentRefs<'FriendsListComponent_user'>; + } + + interface Props { + user: FriendsListComponent_user$key; + } + + return function FriendsList(props: Props) { + const { + data, + loadNext, + loadPrevious, + hasNext, + hasPrevious, + refetch, // For refetching connection + } = useBlockingPaginationFragment( + graphql` + fragment FriendsListComponent_user on User @refetchable(queryName: "FriendsListPaginationQuery") { + name + friends(first: $count, after: $cursor) @connection(key: "FriendsList_user_friends") { + edges { + node { + name + age + } + } + } + } + `, + props.user, + ); + + return ( + <> +

Friends of {data.name}:

+ + {data.friends.edges.map(({ node }) => ( +
+ {node.name} - {node.age} +
+ ))} + + + + ); + }; +}