react-relay
Version:
A framework for building GraphQL-driven React applications.
210 lines (192 loc) • 6.38 kB
Flow
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall relay
*/
;
import type {LoadMoreFn, UseLoadMoreFunctionArgs} from './useLoadMoreFunction';
import type {Options} from './useRefetchableFragmentInternal';
import type {
Disposable,
FragmentType,
GraphQLResponse,
Observer,
RefetchableFragment,
Variables,
} from 'relay-runtime';
const useLoadMoreFunction = require('./useLoadMoreFunction');
const useRefetchableFragmentInternal = require('./useRefetchableFragmentInternal');
const useRelayEnvironment = require('./useRelayEnvironment');
const useStaticFragmentNodeWarning = require('./useStaticFragmentNodeWarning');
const {useCallback, useDebugValue, useState} = require('react');
const {
getFragment,
getFragmentIdentifier,
getPaginationMetadata,
} = require('relay-runtime');
type RefetchVariables<TVariables, TKey: ?{+$fragmentSpreads: mixed, ...}> =
// NOTE: This type ensures that the type of the returned variables is either:
// - nullable if the provided ref type is nullable
// - non-nullable if the provided ref type is non-nullable
[+key: TKey] extends [+key: {+$fragmentSpreads: mixed, ...}]
? Partial<TVariables>
: TVariables;
type RefetchFnBase<TVars, TOptions> = (
vars: TVars,
options?: TOptions,
) => Disposable;
export type RefetchFn<TVariables, TKey, TOptions = Options> = RefetchFnBase<
RefetchVariables<TVariables, TKey>,
TOptions,
>;
export type ReturnType<TVariables, TData, TKey> = {
// NOTE: This type 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
data: [+key: TKey] extends [+key: {+$fragmentSpreads: mixed, ...}]
? TData
: ?TData,
loadNext: LoadMoreFn<TVariables>,
loadPrevious: LoadMoreFn<TVariables>,
hasNext: boolean,
hasPrevious: boolean,
isLoadingNext: boolean,
isLoadingPrevious: boolean,
refetch: RefetchFn<TVariables, TKey>,
};
// This separate type export is only needed as long as we are injecting
// a separate hooks implementation in ./HooksImplementation -- it can
// be removed after we stop doing that.
export type UsePaginationFragmentType = <
TFragmentType: FragmentType,
TVariables: Variables,
TData,
TKey: ?{+$fragmentSpreads: TFragmentType, ...},
>(
fragmentInput: RefetchableFragment<TFragmentType, TData, TVariables>,
parentFragmentRef: TKey,
) => ReturnType<TVariables, TData, TKey>;
hook usePaginationFragment<
TFragmentType: FragmentType,
TVariables: Variables,
TData,
TKey: ?{+$fragmentSpreads: TFragmentType, ...},
>(
fragmentInput: RefetchableFragment<TFragmentType, TData, TVariables>,
parentFragmentRef: TKey,
): ReturnType<TVariables, TData, TKey> {
const fragmentNode = getFragment(fragmentInput);
useStaticFragmentNodeWarning(
fragmentNode,
'first argument of usePaginationFragment()',
);
const componentDisplayName = 'usePaginationFragment()';
const {connectionPathInFragmentData, paginationRequest, paginationMetadata} =
getPaginationMetadata(fragmentNode, componentDisplayName);
const {fragmentData, fragmentRef, refetch} = useRefetchableFragmentInternal<
{variables: TVariables, response: TData},
{data?: TData},
>(fragmentNode, parentFragmentRef, componentDisplayName);
const fragmentIdentifier = getFragmentIdentifier(fragmentNode, fragmentRef);
// Backward pagination
const [loadPrevious, hasPrevious, isLoadingPrevious, disposeFetchPrevious] =
useLoadMore<TVariables>({
componentDisplayName,
connectionPathInFragmentData,
direction: 'backward',
fragmentData,
fragmentIdentifier,
fragmentNode,
fragmentRef,
paginationMetadata,
paginationRequest,
});
// Forward pagination
const [loadNext, hasNext, isLoadingNext, disposeFetchNext] =
useLoadMore<TVariables>({
componentDisplayName,
connectionPathInFragmentData,
direction: 'forward',
fragmentData,
fragmentIdentifier,
fragmentNode,
fragmentRef,
paginationMetadata,
paginationRequest,
});
const refetchPagination = useCallback(
(variables: TVariables, options: void | Options) => {
disposeFetchNext();
disposeFetchPrevious();
return refetch(variables, {...options, __environment: undefined});
},
[disposeFetchNext, disposeFetchPrevious, refetch],
);
if (__DEV__) {
// eslint-disable-next-line react-hooks/rules-of-hooks
// $FlowFixMe[react-rule-hook]
useDebugValue({
fragment: fragmentNode.name,
data: fragmentData,
hasNext,
isLoadingNext,
hasPrevious,
isLoadingPrevious,
});
}
return {
// $FlowFixMe[incompatible-return]
data: fragmentData,
loadNext,
loadPrevious,
hasNext,
hasPrevious,
isLoadingNext,
isLoadingPrevious,
refetch: refetchPagination,
};
}
hook useLoadMore<TVariables: Variables>(
args: $Diff<
UseLoadMoreFunctionArgs,
{
observer: Observer<GraphQLResponse>,
onReset: () => void,
...
},
>,
): [LoadMoreFn<TVariables>, boolean, boolean, () => void] {
const environment = useRelayEnvironment();
const [isLoadingMore, reallySetIsLoadingMore] = useState(false);
// Schedule this update since it must be observed by components at the same
// batch as when hasNext changes. hasNext is read from the store and store
// updates are scheduled, so this must be scheduled too.
const setIsLoadingMore = (value: boolean) => {
const schedule = environment.getScheduler()?.schedule;
if (schedule) {
schedule(() => {
reallySetIsLoadingMore(value);
});
} else {
reallySetIsLoadingMore(value);
}
};
const observer = {
start: () => setIsLoadingMore(true),
complete: () => setIsLoadingMore(false),
error: () => setIsLoadingMore(false),
};
const handleReset = () => setIsLoadingMore(false);
const [loadMore, hasMore, disposeFetch] = useLoadMoreFunction<TVariables>({
...args,
observer,
onReset: handleReset,
});
return [loadMore, hasMore, isLoadingMore, disposeFetch];
}
module.exports = usePaginationFragment;