diff --git a/types/react-relay/hooks.d.ts b/types/react-relay/hooks.d.ts new file mode 100644 index 0000000000..e4ca27e8e5 --- /dev/null +++ b/types/react-relay/hooks.d.ts @@ -0,0 +1 @@ +export * from './lib/hooks'; diff --git a/types/react-relay/index.d.ts b/types/react-relay/index.d.ts index 45e536002e..f89fc5c766 100644 --- a/types/react-relay/index.d.ts +++ b/types/react-relay/index.d.ts @@ -1,4 +1,4 @@ -// Type definitions for react-relay 6.0 +// Type definitions for react-relay 7.0 // Project: https://github.com/facebook/relay, https://facebook.github.io/relay // Definitions by: Johannes Schickling // Matt Martin @@ -8,8 +8,9 @@ // Kaare Hoff Skovgaard // Matt Krick // Jared Kass +// Renan Machado // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 3.0 +// TypeScript Version: 3.3 import * as React from 'react'; import { @@ -117,13 +118,17 @@ interface QueryRendererProps { }) => React.ReactNode; variables: TOperation['variables']; } -declare class ReactRelayQueryRenderer extends React.Component<{ - cacheConfig?: CacheConfig | null; - fetchPolicy?: FetchPolicy; -} & QueryRendererProps> {} +declare class ReactRelayQueryRenderer extends React.Component< + { + cacheConfig?: CacheConfig | null; + fetchPolicy?: FetchPolicy; + } & QueryRendererProps +> {} export { ReactRelayQueryRenderer as QueryRenderer }; -declare class ReactRelayLocalQueryRenderer extends React.Component> {} +declare class ReactRelayLocalQueryRenderer extends React.Component< + QueryRendererProps +> {} export { ReactRelayLocalQueryRenderer as LocalQueryRenderer }; export const ReactRelayContext: React.Context; diff --git a/types/react-relay/lib/hooks.d.ts b/types/react-relay/lib/hooks.d.ts new file mode 100644 index 0000000000..8533f252e0 --- /dev/null +++ b/types/react-relay/lib/hooks.d.ts @@ -0,0 +1,17 @@ +export { graphql } from 'relay-runtime'; +export { fetchQuery } from './relay-experimental/fetchQuery'; + +export { EntryPointContainer } from './relay-experimental/EntryPointContainer'; +export { LazyLoadEntryPointContainer } from './relay-experimental/LazyLoadEntryPointContainer'; +export { RelayEnvironmentProvider } from './relay-experimental/RelayEnvironmentProvider'; + +export { preloadQuery } from './relay-experimental/preloadQuery'; +export { prepareEntryPoint } from './relay-experimental/prepareEntryPoint'; + +export { useBlockingPaginationFragment } from './relay-experimental/useBlockingPaginationFragment'; +export { useFragment } from './relay-experimental/useFragment'; +export { useLazyLoadQuery } from './relay-experimental/useLazyLoadQuery'; +export { useLegacyPaginationFragment as usePaginationFragment } from './relay-experimental/useLegacyPaginationFragment'; +export { usePreloadedQuery } from './relay-experimental/usePreloadedQuery'; +export { useRefetchableFragment } from './relay-experimental/useRefetchableFragment'; +export { useRelayEnvironment } from './relay-experimental/useRelayEnvironment'; diff --git a/types/react-relay/lib/relay-experimental/EntryPointContainer.d.ts b/types/react-relay/lib/relay-experimental/EntryPointContainer.d.ts new file mode 100644 index 0000000000..fefd3b3d5a --- /dev/null +++ b/types/react-relay/lib/relay-experimental/EntryPointContainer.d.ts @@ -0,0 +1,24 @@ +/* tslint:disable:no-unnecessary-generics */ + +import { ElementType, ClassicElement } from 'react'; + +import { EntryPointComponent, PreloadedEntryPoint } from './EntryPointTypes'; + +export function EntryPointContainer< + TPreloadedQueries extends {}, + TPreloadedNestedEntryPoints extends {}, + TRuntimeProps extends {}, + TExtraProps, + TEntryPointComponent extends EntryPointComponent< + TPreloadedQueries, + TPreloadedNestedEntryPoints, + TRuntimeProps, + TExtraProps + > +>({ + entryPointReference, + props, +}: Readonly<{ + entryPointReference: PreloadedEntryPoint; + props: TRuntimeProps; +}>): ClassicElement; diff --git a/types/react-relay/lib/relay-experimental/EntryPointTypes.d.ts b/types/react-relay/lib/relay-experimental/EntryPointTypes.d.ts new file mode 100644 index 0000000000..22ba5d5fe8 --- /dev/null +++ b/types/react-relay/lib/relay-experimental/EntryPointTypes.d.ts @@ -0,0 +1,152 @@ +import { + OperationType, + ConcreteRequest, + RequestParameters, + IEnvironment, + Observable, + GraphQLResponse, +} from 'relay-runtime'; + +import { Component } from 'react'; + +export type PreloadFetchPolicy = 'store-or-network' | 'store-and-network' | 'network-only'; + +export interface PreloadOptions { + readonly fetchKey?: string | number; + readonly fetchPolicy?: PreloadFetchPolicy; +} + +// Note: the phantom type parameter here helps ensures that the +// $Parameters.js value matches the type param provided to preloadQuery. +// tslint:disable-next-line interface-over-type-literal +export type PreloadableConcreteRequest = {}; + +export interface EnvironmentProviderOptions { + [key: string]: unknown; +} + +export interface PreloadedQuery< + TQuery extends OperationType, + TEnvironmentProviderOptions = EnvironmentProviderOptions +> { + readonly environment: IEnvironment; + readonly environmentProviderOptions: TEnvironmentProviderOptions | null | undefined; + readonly fetchKey: string | number | null | undefined; + readonly fetchPolicy: PreloadFetchPolicy; + readonly name: string; + readonly source: Observable | null | undefined; + readonly variables: TQuery['variables']; +} + +/** + * The Interface of the EntryPoints .entrypoint files + * + * Every .entrypoint file it's an object that must have two required fields: + * - getPreloadProps(...) function that will return the description of preloaded + * queries and preloaded (nested) entry points for the current entry point + * - root - JSResource of the module that will render those preloaded queries + * + * TEntryPointParams - object that contains all necessary information to execute + * the preloaders (routeParams, query variables) + * + * TPreloadedQueries - queries, defined in the root components + * + * TPreloadedEntryPoints - nested entry points, defined in the root components + * + * TRuntimeProps - the type of additional props that you may pass to the + * component (like `onClick` handler, etc) during runtime. Values for them + * defined during component runtime + * + * TExtraProps - a bag of extra props that you may define in `entrypoint` file + * and they will be passed to the EntryPointComponent as `extraProps` + */ +export type InternalEntryPointRepresentation< + TEntryPointParams, + TPreloadedQueries, + TPreloadedEntryPoints, + TRuntimeProps, + TExtraProps +> = Readonly<{ + getPreloadProps: ( + entryPointParams: TEntryPointParams, + ) => PreloadProps; + root: unknown; +}>; + +// The shape of the props of the entry point `root` component +export type EntryPointProps< + TPreloadedQueries, + TPreloadedEntryPoints = {}, + TRuntimeProps = {}, + TExtraProps = null +> = Readonly<{ + entryPoints: TPreloadedEntryPoints; + extraProps: TExtraProps | null; + props: TRuntimeProps; + queries: TPreloadedQueries; +}>; + +// Type of the entry point `root` component +export type EntryPointComponent< + TPreloadedQueries, + TPreloadedEntryPoints = {}, + TRuntimeProps = {}, + TExtraProps = null +> = Component>; + +// Return type of the `getPreloadProps(...)` of the entry point +export type PreloadProps< + TPreloadParams, + TPreloadedQueries extends {}, + TPreloadedEntryPoints extends {}, + TExtraProps = null, + TEnvironmentProviderOptions = EnvironmentProviderOptions +> = Readonly<{ + entryPoints?: { [T in keyof TPreloadedEntryPoints]: ExtractEntryPointTypeHelper }; + extraProps?: TExtraProps; + queries?: { [T in keyof TPreloadedQueries]: ExtractQueryTypeHelper }; +}>; + +// Return type of the `prepareEntryPoint(...)` function +export type PreloadedEntryPoint = Readonly<{ + entryPoints: JSX.LibraryManagedAttributes; + extraProps: JSX.LibraryManagedAttributes; + getComponent: () => TEntryPointComponent; + queries: JSX.LibraryManagedAttributes; +}>; + +export type ThinQueryParams = Readonly<{ + environmentProviderOptions?: TEnvironmentProviderOptions | null; + options?: PreloadOptions | null; + parameters: PreloadableConcreteRequest; + variables: TQuery['variables']; +}>; + +export type ThinNestedEntryPointParams = Readonly<{ + entryPoint: TEntryPoint; + entryPointParams: TEntryPointParams; +}>; + +export type ExtractQueryTypeHelper = ( + query: PreloadedQuery, +) => ThinQueryParams; + +export type ExtractEntryPointTypeHelper = ( + entryPoint: PreloadedEntryPoint | null | undefined, +) => + | ThinNestedEntryPointParams> + | null + | undefined; + +export type EntryPoint = InternalEntryPointRepresentation< + TEntryPointParams, + JSX.LibraryManagedAttributes, + JSX.LibraryManagedAttributes, + JSX.LibraryManagedAttributes, + JSX.LibraryManagedAttributes +>; + +// tslint:disable-next-line interface-name +export interface IEnvironmentProvider { + getEnvironment(options: TOptions | null): IEnvironment; +} diff --git a/types/react-relay/lib/relay-experimental/LRUCache.d.ts b/types/react-relay/lib/relay-experimental/LRUCache.d.ts new file mode 100644 index 0000000000..e5aad6efdf --- /dev/null +++ b/types/react-relay/lib/relay-experimental/LRUCache.d.ts @@ -0,0 +1,46 @@ +export interface Cache { + get(key: string): T | null; + set(key: string, value: T): void; + has(key: string): boolean; + delete(key: string): void; + size(): number; + capacity(): number; + clear(): void; +} + +/** + * JS maps (both plain objects and Map) maintain key insertion + * order, which means there is an easy way to simulate LRU behavior + * that should also perform quite well: + * + * To insert a new value, first delete the key from the inner _map, + * then _map.set(k, v). By deleting and reinserting, you ensure that the + * map sees the key as the last inserted key. + * + * Get does the same: if the key is present, delete and reinsert it. + */ +declare class LRUCache implements Cache { + _capacity: number; + _map: Map; + + constructor(capacity: number); + + set(key: string, value: T): void; + + get(key: string): T | null; + + has(key: string): boolean; + + delete(key: string): void; + + size(): number; + + capacity(): number; + + clear(): void; +} + +// tslint:disable-next-line:no-unnecessary-generics +declare function create(capacity: number): LRUCache; + +export { create }; diff --git a/types/react-relay/lib/relay-experimental/LazyLoadEntryPointContainer.d.ts b/types/react-relay/lib/relay-experimental/LazyLoadEntryPointContainer.d.ts new file mode 100644 index 0000000000..58170258df --- /dev/null +++ b/types/react-relay/lib/relay-experimental/LazyLoadEntryPointContainer.d.ts @@ -0,0 +1,43 @@ +import { EntryPoint, EntryPointComponent, EnvironmentProviderOptions, IEnvironmentProvider } from './EntryPointTypes'; + +export type EntryPointContainerProps< + TEntryPointParams, + TPreloadedQueries, + TPreloadedEntryPoints, + TRuntimeProps, + TExtraProps +> = Readonly< + Readonly<{ + entryPoint: EntryPoint< + TEntryPointParams, + EntryPointComponent + >; + entryPointParams: TEntryPointParams; + environmentProvider?: IEnvironmentProvider; + props: TRuntimeProps; + }> +>; + +export function LazyLoadEntryPointContainer< + TEntryPointParams extends {}, + TPreloadedQueries extends {}, + TPreloadedEntryPoints extends {}, + TRuntimeProps extends {}, + TExtraProps +>({ + entryPoint, + entryPointParams, + props, + environmentProvider, +}: EntryPointContainerProps< + // tslint:disable-next-line no-unnecessary-generics + TEntryPointParams, + // tslint:disable-next-line no-unnecessary-generics + TPreloadedQueries, + // tslint:disable-next-line no-unnecessary-generics + TPreloadedEntryPoints, + // tslint:disable-next-line no-unnecessary-generics + TRuntimeProps, + // tslint:disable-next-line no-unnecessary-generics + TExtraProps +>): JSX.Element; diff --git a/types/react-relay/lib/relay-experimental/QueryResource.d.ts b/types/react-relay/lib/relay-experimental/QueryResource.d.ts new file mode 100644 index 0000000000..506884f9de --- /dev/null +++ b/types/react-relay/lib/relay-experimental/QueryResource.d.ts @@ -0,0 +1,84 @@ +import { + Disposable, + FragmentPointer, + GraphQLResponse, + IEnvironment, + Observable, + Observer, + OperationDescriptor, + ReaderFragment, + Snapshot, +} from 'relay-runtime'; +import { Cache } from './LRUCache'; + +export type QueryResource = QueryResourceImpl; +export type FetchPolicy = 'store-only' | 'store-or-network' | 'store-and-network' | 'network-only'; +export type RenderPolicy = 'full' | 'partial'; + +type QueryResourceCache = Cache; +interface QueryResourceCacheEntry { + readonly cacheKey: string; + getRetainCount(): number; + getValue(): Error | Promise | QueryResult; + setValue(value: Error | Promise | QueryResult): void; + temporaryRetain(environment: IEnvironment): void; + permanentRetain(environment: IEnvironment): Disposable; +} +interface QueryResult { + cacheKey: string; + fragmentNode: ReaderFragment; + fragmentRef: FragmentPointer; + operation: OperationDescriptor; +} + +declare function getQueryCacheKey( + operation: OperationDescriptor, + fetchPolicy: FetchPolicy, + renderPolicy: RenderPolicy, +): string; + +declare function getQueryResult(operation: OperationDescriptor, cacheKey: string): QueryResult; + +declare function createQueryResourceCacheEntry( + cacheKey: string, + operation: OperationDescriptor, + value: Error | Promise | QueryResult, + onDispose: (entry: QueryResourceCacheEntry) => void, +): QueryResourceCacheEntry; + +declare class QueryResourceImpl { + constructor(environment: IEnvironment); + + /** + * This function should be called during a Component's render function, + * to either read an existing cached value for the query, or fetch the query + * and suspend. + */ + prepare( + operation: OperationDescriptor, + fetchObservable: Observable, + maybeFetchPolicy: FetchPolicy | null, + maybeRenderPolicy: RenderPolicy | null, + observer?: Observer, + cacheKeyBuster?: string | number, + ): QueryResult; + + /** + * This function should be called during a Component's commit phase + * (e.g. inside useEffect), in order to retain the operation in the Relay store + * and transfer ownership of the operation to the component lifecycle. + */ + retain(queryResult: QueryResult): Disposable; + + getCacheEntry( + operation: OperationDescriptor, + fetchPolicy: FetchPolicy, + maybeRenderPolicy?: RenderPolicy, + ): QueryResourceCacheEntry | null; +} + +declare function createQueryResource(environment: IEnvironment): QueryResource; + +declare function getQueryResourceForEnvironment(environment: IEnvironment): QueryResourceImpl; + +export { createQueryResource, getQueryResourceForEnvironment }; diff --git a/types/react-relay/lib/relay-experimental/RelayEnvironmentProvider.d.ts b/types/react-relay/lib/relay-experimental/RelayEnvironmentProvider.d.ts new file mode 100644 index 0000000000..abb1ee8636 --- /dev/null +++ b/types/react-relay/lib/relay-experimental/RelayEnvironmentProvider.d.ts @@ -0,0 +1,9 @@ +import { ReactNode } from 'react'; +import { IEnvironment } from 'relay-runtime'; + +export interface Props { + children: ReactNode; + environment: IEnvironment; +} + +export function RelayEnvironmentProvider(props: Props): JSX.Element; diff --git a/types/react-relay/lib/relay-experimental/fetchQuery.d.ts b/types/react-relay/lib/relay-experimental/fetchQuery.d.ts new file mode 100644 index 0000000000..c336ce9cee --- /dev/null +++ b/types/react-relay/lib/relay-experimental/fetchQuery.d.ts @@ -0,0 +1,85 @@ +import { CacheConfig, GraphQLTaggedNode, IEnvironment, Observable, OperationType } from 'relay-runtime'; + +/** + * Fetches the given query and variables on the provided environment, + * and de-dupes identical in-flight requests. + * + * Observing a request: + * ==================== + * fetchQuery returns an Observable which you can call .subscribe() + * on. Subscribe optionally takes an Observer, which you can provide to + * observe network events: + * + * ``` + * fetchQuery(environment, query, variables).subscribe({ + * // Called when network requests starts + * start: (subsctiption) => {}, + * + * // Called after a payload is received and written to the local store + * next: (payload) => {}, + * + * // Called when network requests errors + * error: (error) => {}, + * + * // Called when network requests fully completes + * complete: () => {}, + * + * // Called when network request is unsubscribed + * unsubscribe: (subscription) => {}, + * }); + * ``` + * + * Request Promise: + * ================ + * The obervable can be converted to a Promise with .toPromise(), which will + * resolve to a snapshot of the query data when the first response is received + * from the server. + * + * ``` + * fetchQuery(environment, query, variables).then((data) => { + * // ... + * }); + * ``` + * + * In-flight request de-duping: + * ============================ + * By default, calling fetchQuery multiple times with the same + * environment, query and variables will not initiate a new request if a request + * for those same parameters is already in flight. + * + * A request is marked in-flight from the moment it starts until the moment it + * fully completes, regardless of error or successful completion. + * + * NOTE: If the request completes _synchronously_, calling fetchQuery + * a second time with the same arguments in the same tick will _NOT_ de-dupe + * the request given that it will no longer be in-flight. + * + * + * Data Retention: + * =============== + * This function will NOT retain query data, meaning that it is not guaranteed + * that the fetched data will remain in the Relay store after the request has + * completed. + * If you need to retain the query data outside of the network request, + * you need to use `environment.retain()`. + * + * + * Cancelling requests: + * ==================== + * If the disposable returned by subscribe is called while the + * request is in-flight, the request will be cancelled. + * + * ``` + * const disposable = fetchQuery(...).subscribe(...); + * + * // This will cancel the request if it is in-flight. + * disposable.dispose(); + * ``` + * NOTE: When using .toPromise(), the request cannot be cancelled. + */ +export function fetchQuery( + environment: IEnvironment, + query: GraphQLTaggedNode, + variables: TQuery['variables'], + options?: { networkCacheConfig?: CacheConfig }, +): Observable; diff --git a/types/react-relay/lib/relay-experimental/preloadQuery.d.ts b/types/react-relay/lib/relay-experimental/preloadQuery.d.ts new file mode 100644 index 0000000000..6203c3f69c --- /dev/null +++ b/types/react-relay/lib/relay-experimental/preloadQuery.d.ts @@ -0,0 +1,10 @@ +import { GraphQLResponse, IEnvironment, OperationType, Subscription } from 'relay-runtime'; +import { PreloadableConcreteRequest, PreloadedQuery, PreloadFetchPolicy, PreloadOptions } from './EntryPointTypes'; + +export function preloadQuery( + environment: IEnvironment, + preloadableRequest: PreloadableConcreteRequest, + variables: TQuery['variables'], + options?: PreloadOptions | null, + environmentProviderOptions?: TEnvironmentProviderOptions | null, +): PreloadedQuery; diff --git a/types/react-relay/lib/relay-experimental/prepareEntryPoint.d.ts b/types/react-relay/lib/relay-experimental/prepareEntryPoint.d.ts new file mode 100644 index 0000000000..309600c29f --- /dev/null +++ b/types/react-relay/lib/relay-experimental/prepareEntryPoint.d.ts @@ -0,0 +1,32 @@ +import { + EntryPoint, + EntryPointComponent, + EnvironmentProviderOptions, + IEnvironmentProvider, + PreloadedEntryPoint, +} from './EntryPointTypes'; + +export function prepareEntryPoint< + TEntryPointParams extends {}, + TPreloadedQueries extends {}, + TPreloadedEntryPoints extends {}, + TRuntimeProps extends {}, + TExtraProps, + TEntryPointComponent extends EntryPointComponent< + // tslint:disable-next-line no-unnecessary-generics + TPreloadedQueries, + // tslint:disable-next-line no-unnecessary-generics + TPreloadedEntryPoints, + // tslint:disable-next-line no-unnecessary-generics + TRuntimeProps, + // tslint:disable-next-line no-unnecessary-generics + TExtraProps + >, + // tslint:disable-next-line no-unnecessary-generics + TEntryPoint extends EntryPoint +>( + environmentProvider: IEnvironmentProvider, + // tslint:disable-next-line no-unnecessary-generics + entryPoint: TEntryPoint, + entryPointParams: TEntryPointParams, +): PreloadedEntryPoint; diff --git a/types/react-relay/lib/relay-experimental/useBlockingPaginationFragment.d.ts b/types/react-relay/lib/relay-experimental/useBlockingPaginationFragment.d.ts new file mode 100644 index 0000000000..d30b97e8b2 --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useBlockingPaginationFragment.d.ts @@ -0,0 +1,36 @@ +import { GraphQLResponse, GraphQLTaggedNode, Observer, OperationType } from 'relay-runtime'; + +import { LoadMoreFn, UseLoadMoreFunctionArgs } from './useLoadMoreFunction'; +import { RefetchFnDynamic } from './useRefetchableFragmentNode'; + +export interface ReturnType { + data: TFragmentData; + loadNext: LoadMoreFn; + loadPrevious: LoadMoreFn; + hasNext: boolean; + hasPrevious: boolean; + refetch: RefetchFnDynamic; +} + +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; + +export function useBlockingPaginationFragment< + TQuery extends OperationType, + TKey extends { readonly ' $data'?: unknown | null } +>( + 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> +>; diff --git a/types/react-relay/lib/relay-experimental/useFragment.d.ts b/types/react-relay/lib/relay-experimental/useFragment.d.ts new file mode 100644 index 0000000000..aacd5794eb --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useFragment.d.ts @@ -0,0 +1,41 @@ +import { GraphQLTaggedNode } from 'relay-runtime'; + +// NOTE: These declares ensure that the type of the returned data is: +// - non-nullable if the provided ref type is non-nullable +// - nullable if the provided ref type is nullable +// - array of non-nullable if the privoided ref type is an array of +// non-nullable refs +// - array of nullable if the privoided ref type is an array of nullable refs + +type $Call any> = Fn extends (arg: any) => infer RT ? RT : never; + +type NonNullableReturnType = (arg: T) => NonNullable; +type NullableReturnType = (arg: T) => T[' $data'] | null; +type NonNullableArrayReturnType> = (arg: { + readonly ' $data': T; +}) => Array>; +type NullableArrayReturnType> = ( + arg: T, +) => Array | null; + +export function useFragment( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey, +): $Call>; + +export function useFragment( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey, +): $Call>; + +export function useFragment>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey, +): $Call>; + +export function useFragment>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey, +): $Call>; + +export {}; diff --git a/types/react-relay/lib/relay-experimental/useLazyLoadQuery.d.ts b/types/react-relay/lib/relay-experimental/useLazyLoadQuery.d.ts new file mode 100644 index 0000000000..3418e00479 --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useLazyLoadQuery.d.ts @@ -0,0 +1,13 @@ +import { FetchPolicy, RenderPolicy } from './QueryResource'; +import { CacheConfig, GraphQLTaggedNode, OperationType } from 'relay-runtime'; + +export function useLazyLoadQuery( + gqlQuery: GraphQLTaggedNode, + variables: TQuery['variables'], + options?: { + fetchKey?: string | number; + fetchPolicy?: FetchPolicy; + networkCacheConfig?: CacheConfig; + renderPolicy_UNSTABLE?: RenderPolicy; + }, +): TQuery['response']; diff --git a/types/react-relay/lib/relay-experimental/useLegacyPaginationFragment.d.ts b/types/react-relay/lib/relay-experimental/useLegacyPaginationFragment.d.ts new file mode 100644 index 0000000000..0f0021eeeb --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useLegacyPaginationFragment.d.ts @@ -0,0 +1,28 @@ +import { LoadMoreFn, UseLoadMoreFunctionArgs } from './useLoadMoreFunction'; +import { RefetchFnDynamic } from './useRefetchableFragmentNode'; +import { GraphQLResponse, GraphQLTaggedNode, Observer, OperationType } from 'relay-runtime'; + +export interface ReturnType { + data: TFragmentData; + loadNext: LoadMoreFn; + loadPrevious: LoadMoreFn; + hasNext: boolean; + hasPrevious: boolean; + isLoadingNext: boolean; + isLoadingPrevious: boolean; + refetch: RefetchFnDynamic; +} + +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; + +export function useLegacyPaginationFragment< + TQuery extends OperationType, + TKey extends { readonly ' $data'?: unknown | null } +>( + fragmentInput: GraphQLTaggedNode, + parentFragmentRef: TKey, +): // tslint:disable-next-line no-unnecessary-generics +ReturnType & NullableReturnType>>; diff --git a/types/react-relay/lib/relay-experimental/useLoadMoreFunction.d.ts b/types/react-relay/lib/relay-experimental/useLoadMoreFunction.d.ts new file mode 100644 index 0000000000..d3137ebd2e --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useLoadMoreFunction.d.ts @@ -0,0 +1,45 @@ +import { + ConcreteRequest, + Disposable, + GraphQLResponse, + Observer, + ReaderFragment, + ReaderPaginationMetadata, + RequestDescriptor, +} from 'relay-runtime'; + +export type Direction = 'forward' | 'backward'; + +export type LoadMoreFn = ( + count: number, + options?: { + onComplete?: (arg: Error | null) => void; + }, +) => Disposable; + +export interface UseLoadMoreFunctionArgs { + direction: Direction; + fragmentNode: ReaderFragment; + fragmentIdentifier: string; + fragmentOwner: RequestDescriptor | ReadonlyArray | null; + fragmentData: unknown; + connectionPathInFragmentData: ReadonlyArray; + fragmentRefPathInResponse: ReadonlyArray; + paginationRequest: ConcreteRequest; + paginationMetadata: ReaderPaginationMetadata; + componentDisplayName: string; + observer: Observer; + onReset: () => void; +} + +export function useLoadMoreFunction(args: UseLoadMoreFunctionArgs): [LoadMoreFn, boolean, () => void]; + +export function getConnectionState( + direction: Direction, + fragmentNode: ReaderFragment, + fragmentData: unknown, + connectionPathInFragmentData: ReadonlyArray, +): { + cursor: string | null; + hasMore: boolean; +}; diff --git a/types/react-relay/lib/relay-experimental/usePreloadedQuery.d.ts b/types/react-relay/lib/relay-experimental/usePreloadedQuery.d.ts new file mode 100644 index 0000000000..4e0ba2a914 --- /dev/null +++ b/types/react-relay/lib/relay-experimental/usePreloadedQuery.d.ts @@ -0,0 +1,8 @@ +import { GraphQLTaggedNode, OperationType } from 'relay-runtime'; + +import { PreloadedQuery } from './EntryPointTypes'; + +export function usePreloadedQuery( + gqlQuery: GraphQLTaggedNode, + preloadedQuery: PreloadedQuery, +): TQuery['response']; diff --git a/types/react-relay/lib/relay-experimental/useRefetchableFragment.d.ts b/types/react-relay/lib/relay-experimental/useRefetchableFragment.d.ts new file mode 100644 index 0000000000..e22c5766fb --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useRefetchableFragment.d.ts @@ -0,0 +1,18 @@ +import { RefetchFnDynamic } from './useRefetchableFragmentNode'; +import { GraphQLTaggedNode, OperationType } from 'relay-runtime'; + +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; + +export type ReturnType = [ + $Call & NullableReturnType>, + RefetchFnDynamic, +]; + +export function useRefetchableFragment< + TQuery extends OperationType, + TKey extends { readonly ' $data'?: unknown | null } + // tslint:disable-next-line:no-unnecessary-generics +>(fragmentInput: GraphQLTaggedNode, fragmentRef: TKey): ReturnType; diff --git a/types/react-relay/lib/relay-experimental/useRefetchableFragmentNode.d.ts b/types/react-relay/lib/relay-experimental/useRefetchableFragmentNode.d.ts new file mode 100644 index 0000000000..af42a685e9 --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useRefetchableFragmentNode.d.ts @@ -0,0 +1,124 @@ +import { Disposable, OperationType, IEnvironment, Variables, ReaderFragment } from 'relay-runtime'; + +import { FetchPolicy, RenderPolicy } from './QueryResource'; + +export type RefetchFn = RefetchFnExact; + +export type RefetchFnDynamic< + TQuery extends OperationType, + TKey extends { readonly [key: string]: any } | null, + TOptions = Options +> = RefetchInexactDynamicResponse & RefetchExactDynamicResponse; + +export type RefetchInexact = ( + data?: unknown, +) => RefetchFnInexact; +export type RefetchInexactDynamicResponse = ReturnType< + RefetchInexact +>; + +export type RefetchExact = ( + data?: unknown | null, +) => RefetchFnExact; +export type RefetchExactDynamicResponse = ReturnType< + RefetchExact +>; + +export type RefetchFnBase = (vars: TVars, options?: TOptions) => Disposable; + +export type RefetchFnExact = RefetchFnBase< + TQuery['variables'], + TOptions +>; +export type RefetchFnInexact = RefetchFnBase< + TQuery['variables'], + TOptions +>; + +export interface ReturnTypeNode< + TQuery extends OperationType, + TKey extends { readonly [key: string]: any } | null, + TOptions = Options +> { + fragmentData: unknown; + fragmentRef: unknown; + refetch: RefetchFnDynamic; + disableStoreUpdates: () => void; + enableStoreUpdates: () => void; +} + +export interface Options { + fetchPolicy?: FetchPolicy; + onComplete?: (arg: Error | null) => void; +} + +export interface InternalOptions extends Options { + __environment?: IEnvironment; + renderPolicy?: RenderPolicy; +} + +export type Action = + | { + type: string; + environment: IEnvironment; + fragmentIdentifier: string; + } + | { + type: string; + refetchVariables: Variables; + fetchPolicy?: FetchPolicy; + renderPolicy?: RenderPolicy; + onComplete?: (args: Error | null) => void; + environment: IEnvironment; + }; + +export interface RefetchState { + fetchPolicy: FetchPolicy | undefined; + renderPolicy: RenderPolicy | undefined; + mirroredEnvironment: IEnvironment; + mirroredFragmentIdentifier: string; + onComplete: ((arg: Error | null) => void) | undefined; + refetchEnvironment?: IEnvironment | null; + refetchVariables?: Variables | null; +} + +export interface DebugIDandTypename { + id: string; + typename: string; +} + +export function reducer(state: RefetchState, action: Action): RefetchState; + +export function useRefetchableFragmentNode< + TQuery extends OperationType, + TKey extends { readonly [key: string]: any } | null +>( + fragmentNode: ReaderFragment, + parentFragmentRef: unknown, + componentDisplayName: string, +): // tslint:disable-next-line:no-unnecessary-generics +ReturnTypeNode; + +export function useRefetchFunction( + fragmentNode: any, + parentFragmentRef: any, + fragmentIdentifier: any, + fragmentRefPathInResponse: any, + fragmentData: any, + refetchGenerationRef: any, + dispatch: any, + disposeFetch: any, + componentDisplayName: any, +): // tslint:disable-next-line:no-unnecessary-generics +RefetchFn; + +export function readQuery( + environment: any, + query: any, + fetchPolicy: any, + renderPolicy: any, + refetchGeneration: any, + componentDisplayName: any, + { start, complete }: any, + profilerContext: any, +): any; diff --git a/types/react-relay/lib/relay-experimental/useRelayEnvironment.d.ts b/types/react-relay/lib/relay-experimental/useRelayEnvironment.d.ts new file mode 100644 index 0000000000..ec9402d222 --- /dev/null +++ b/types/react-relay/lib/relay-experimental/useRelayEnvironment.d.ts @@ -0,0 +1,3 @@ +import { Environment } from 'relay-runtime'; + +export function useRelayEnvironment(): Environment; diff --git a/types/react-relay/test/relay-experimental-tests.tsx b/types/react-relay/test/relay-experimental-tests.tsx new file mode 100644 index 0000000000..a8a24f9d4b --- /dev/null +++ b/types/react-relay/test/relay-experimental-tests.tsx @@ -0,0 +1,400 @@ +import * as React from 'react'; + +import { Environment, RecordSource, Store, Network, commitMutation, FragmentRefs } from 'relay-runtime'; +import { + fetchQuery, + graphql, + RelayEnvironmentProvider, + useRelayEnvironment, + preloadQuery, + usePreloadedQuery, + useLazyLoadQuery, + useFragment, + useRefetchableFragment, + usePaginationFragment, +} from 'react-relay/hooks'; + +const source = new RecordSource(); +const store = new Store(source); + +function cacheHandler(operation: any, variables: { [key: string]: string }, cacheConfig: {}) { + return fetch('/graphql', { + method: 'POST', + body: JSON.stringify({ + query: operation.text, // GraphQL text from input + variables, + }), + }).then((response: any) => { + return response.json(); + }); +} + +const network = Network.create(cacheHandler); + +const environment = new Environment({ + network, + store, +}); + +const query = graphql` + query SomeQuery { + someType + } +`; + +const variables = {}; + +/** + * Test of fetchQuery + */ +const dispose = fetchQuery(environment, query, variables).subscribe({ + start: subsctiption => {}, + next: payload => {}, + error: (error: Error) => {}, + complete: () => {}, + unsubscribe: subscription => {}, +}); + +dispose.unsubscribe(); + +interface AppQueryVariables { + id: string; +} +interface AppQueryResponse { + readonly user: { + readonly name: string; + } | null; +} +interface AppQuery { + readonly response: AppQueryResponse; + readonly variables: AppQueryVariables; +} + +/** + * Test of RelayEnvironmentProvider + * see https://relay.dev/docs/en/experimental/api-reference#relayenvironmentprovider + */ +function Provider() { + return ( + +
+ + ); +} + +/** + * Test of useRelayEnvironment + * see https://relay.dev/docs/en/experimental/api-reference#userelayenvironment + */ +function RelayEnvironment() { + const environment = useRelayEnvironment(); + + const handler = React.useCallback(() => { + const mutation = graphql` + mutation AddTodoMutation($input: AddTodoInput!) { + addTodo(input: $input) { + todoEdge { + __typename + cursor + node { + complete + id + text + } + } + viewer { + id + totalCount + } + } + } + `; + + commitMutation(environment, { mutation, variables: {} }); + }, [environment]); + + return ( + Loading...
}> +
+ + ); +} + +/** + * Tests of usePreloadedQuery + * see https://relay.dev/docs/en/experimental/api-reference#usepreloadedquery + */ +function PreloadedQuery() { + const appQuery = graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } + `; + + const result = preloadQuery(environment, appQuery, { id: '4' }, { fetchPolicy: 'store-or-network' }); + + return function App() { + const data = usePreloadedQuery(query, result); + + if (!data.user) return; + return

{data.user.name}

; + }; +} + +/** + * Tests for useLazyLoadQuery + * see https://relay.dev/docs/en/experimental/api-reference#uselazyloadquery + */ +function LazyLoadQuery() { + return function App() { + const data = useLazyLoadQuery( + graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } + `, + { id: '4' }, + { fetchPolicy: 'store-and-network', networkCacheConfig: { force: true } }, + ); + + return

{data.user!.name}

; + }; +} + +/** + * Tests for useFragment + * see https://relay.dev/docs/en/experimental/api-reference#usefragment + */ +function NonNullableFragment() { + interface UserComponent_user { + readonly id: string; + readonly name: string; + readonly profile_picture: { + readonly uri: string; + }; + readonly ' $refType': 'UserComponent_user'; + } + type UserComponent_user$data = UserComponent_user; + interface UserComponent_user$key { + readonly ' $data'?: UserComponent_user$data; + readonly ' $fragmentRefs': FragmentRefs<'UserComponent_user'>; + } + + interface Props { + user: UserComponent_user$key; + } + + return function UserComponent(props: Props) { + const data = useFragment( + graphql` + fragment UserComponent_user on User { + name + profile_picture(scale: 2) { + uri + } + } + `, + props.user, + ); + + return ( + <> +

{data.name}

+
+ +
+ + ); + }; +} +function NullableFragment() { + interface UserComponent_user { + readonly id: string; + readonly name: string | null; + readonly profile_picture: { + readonly uri: string | null; + } | null; + readonly ' $refType': 'UserComponent_user'; + } + type UserComponent_user$data = UserComponent_user; + interface UserComponent_user$key { + readonly ' $data'?: UserComponent_user$data; + readonly ' $fragmentRefs': FragmentRefs<'UserComponent_user'>; + } + + interface Props { + user: UserComponent_user$key; + } + + return function UserComponent(props: Props) { + const data = useFragment( + graphql` + fragment UserComponent_user on User { + name + profile_picture(scale: 2) { + uri + } + } + `, + props.user, + ); + + return ( + <> +

{data.name}

+
+ +
+ + ); + }; +} + +/** + * Tests for useRefetchableFragment + * see https://relay.dev/docs/en/experimental/api-reference#userefetchablefragment + */ +function RefetchableFragment() { + interface CommentBodyRefetchQueryVariables { + lang?: string | null; + id?: string | null; + } + interface CommentBodyRefetchQueryResponse { + readonly node: { + readonly ' $fragmentRefs': FragmentRefs<'CommentBody_comment'>; + } | null; + } + interface CommentBodyRefetchQuery { + readonly response: CommentBodyRefetchQueryResponse; + readonly variables: CommentBodyRefetchQueryVariables; + } + + interface CommentBody_comment { + readonly body: { + readonly text: string; + } | null; + readonly id: string | null; + readonly ' $refType': 'CommentBody_comment'; + } + type CommentBody_comment$data = CommentBody_comment; + interface CommentBody_comment$key { + readonly ' $data'?: CommentBody_comment$data; + readonly ' $fragmentRefs': FragmentRefs<'CommentBody_comment'>; + } + + interface Props { + comment: CommentBody_comment$key; + } + + return function CommentBody(props: Props) { + const [data, refetch] = useRefetchableFragment( + graphql` + fragment CommentBody_comment on Comment @refetchable(queryName: "CommentBodyRefetchQuery") { + body(lang: $lang) { + text + } + } + `, + props.comment, + ); + + return ( + <> +

{data!.body!.text}

+ + + ); + }; +} + +/** + * Tests for usePaginationFragment + * see https://relay.dev/docs/en/experimental/api-reference#userefetchablefragment + */ +function PaginationFragment() { + 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} +
+ ))} + + + + ); + }; +} diff --git a/types/react-relay/tsconfig.json b/types/react-relay/tsconfig.json index 451f6fbd3e..f4f7d0c488 100644 --- a/types/react-relay/tsconfig.json +++ b/types/react-relay/tsconfig.json @@ -1,25 +1,17 @@ { "compilerOptions": { "module": "commonjs", - "lib": [ - "es6", - "dom" - ], + "lib": ["es6", "dom"], "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "baseUrl": "../", - "typeRoots": [ - "../" - ], + "typeRoots": ["../"], "types": [], "noEmit": true, "forceConsistentCasingInFileNames": true, "jsx": "react" }, - "files": [ - "index.d.ts", - "test/react-relay-tests.tsx" - ] -} \ No newline at end of file + "files": ["index.d.ts", "hooks.d.ts", "test/react-relay-tests.tsx", "test/relay-experimental-tests.tsx"] +}