react-relay
Version:
A framework for building GraphQL-driven React applications.
285 lines (262 loc) • 8.61 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 {
ConcreteRequest,
Direction,
Disposable,
GraphQLResponse,
Observer,
ReaderFragment,
ReaderPaginationMetadata,
Variables,
} from 'relay-runtime';
const getConnectionState = require('./getConnectionState');
const useFetchTrackingRef = require('./useFetchTrackingRef');
const useIsMountedRef = require('./useIsMountedRef');
const useIsOperationNodeActive = require('./useIsOperationNodeActive');
const useLoadMoreFunction_EXPERIMENTAL = require('./useLoadMoreFunction_EXPERIMENTAL');
const useRelayEnvironment = require('./useRelayEnvironment');
const invariant = require('invariant');
const {useCallback, useEffect, useState} = require('react');
const {
__internal: {fetchQuery},
RelayFeatureFlags,
createOperationDescriptor,
getPaginationVariables,
getRefetchMetadata,
getSelector,
} = require('relay-runtime');
const warning = require('warning');
export type LoadMoreFn<TVariables: Variables> = (
count: number,
options?: {
onComplete?: (Error | null) => void,
UNSTABLE_extraVariables?: Partial<TVariables>,
},
) => Disposable;
export type UseLoadMoreFunctionArgs = {
direction: Direction,
fragmentNode: ReaderFragment,
fragmentRef: mixed,
fragmentIdentifier: string,
fragmentData: mixed,
connectionPathInFragmentData: $ReadOnlyArray<string | number>,
paginationRequest: ConcreteRequest,
paginationMetadata: ReaderPaginationMetadata,
componentDisplayName: string,
observer: Observer<GraphQLResponse>,
onReset: () => void,
};
hook useLoadMoreFunction<TVariables: Variables>(
args: UseLoadMoreFunctionArgs,
): [LoadMoreFn<TVariables>, boolean, () => void] {
if (RelayFeatureFlags.ENABLE_ACTIVITY_COMPATIBILITY) {
// $FlowFixMe[react-rule-hook] - the condition is static
return useLoadMoreFunction_EXPERIMENTAL(args);
}
// $FlowFixMe[react-rule-hook] - the condition is static
return useLoadMoreFunction_CURRENT(args);
}
hook useLoadMoreFunction_CURRENT<TVariables: Variables>(
args: UseLoadMoreFunctionArgs,
): [LoadMoreFn<TVariables>, boolean, () => void] {
const {
direction,
fragmentNode,
fragmentRef,
fragmentIdentifier,
fragmentData,
connectionPathInFragmentData,
paginationRequest,
paginationMetadata,
componentDisplayName,
observer,
onReset,
} = args;
const environment = useRelayEnvironment();
const {isFetchingRef, startFetch, disposeFetch, completeFetch} =
useFetchTrackingRef();
const {identifierInfo} = getRefetchMetadata(
fragmentNode,
componentDisplayName,
);
const identifierValue =
identifierInfo?.identifierField != null &&
fragmentData != null &&
typeof fragmentData === 'object'
? fragmentData[identifierInfo.identifierField]
: null;
const isMountedRef = useIsMountedRef();
const [mirroredEnvironment, setMirroredEnvironment] = useState(environment);
const [mirroredFragmentIdentifier, setMirroredFragmentIdentifier] =
useState(fragmentIdentifier);
const isParentQueryActive = useIsOperationNodeActive(
fragmentNode,
fragmentRef,
);
const shouldReset =
environment !== mirroredEnvironment ||
fragmentIdentifier !== mirroredFragmentIdentifier;
if (shouldReset) {
disposeFetch();
onReset();
setMirroredEnvironment(environment);
setMirroredFragmentIdentifier(fragmentIdentifier);
}
const {cursor, hasMore} = getConnectionState(
direction,
fragmentNode,
fragmentData,
connectionPathInFragmentData,
);
// Dispose of pagination requests in flight when unmounting
useEffect(() => {
return () => {
disposeFetch();
};
}, [disposeFetch]);
const loadMore = useCallback(
(
count: number,
options: void | {
UNSTABLE_extraVariables?: Partial<TVariables>,
onComplete?: (Error | null) => void,
},
) => {
// TODO(T41131846): Fetch/Caching policies for loadMore
const onComplete = options?.onComplete;
if (isMountedRef.current !== true) {
// Bail out and warn if we're trying to paginate after the component
// has unmounted
warning(
false,
'Relay: Unexpected fetch on unmounted component for fragment ' +
'`%s` in `%s`. It looks like some instances of your component are ' +
'still trying to fetch data but they already unmounted. ' +
'Please make sure you clear all timers, intervals, ' +
'async calls, etc that may trigger a fetch.',
fragmentNode.name,
componentDisplayName,
);
return {dispose: () => {}};
}
const fragmentSelector = getSelector(fragmentNode, fragmentRef);
if (
isFetchingRef.current === true ||
fragmentData == null ||
isParentQueryActive
) {
if (fragmentSelector == null) {
warning(
false,
'Relay: Unexpected fetch while using a null fragment ref ' +
'for fragment `%s` in `%s`. When fetching more items, we expect ' +
"initial fragment data to be non-null. Please make sure you're " +
'passing a valid fragment ref to `%s` before paginating.',
fragmentNode.name,
componentDisplayName,
componentDisplayName,
);
}
if (onComplete) {
onComplete(null);
}
return {dispose: () => {}};
}
invariant(
fragmentSelector != null &&
fragmentSelector.kind !== 'PluralReaderSelector',
'Relay: Expected to be able to find a non-plural fragment owner for ' +
"fragment `%s` when using `%s`. If you're seeing this, " +
'this is likely a bug in Relay.',
fragmentNode.name,
componentDisplayName,
);
const parentVariables = fragmentSelector.owner.variables;
const fragmentVariables = fragmentSelector.variables;
const extraVariables = options?.UNSTABLE_extraVariables;
const baseVariables = {
...parentVariables,
...fragmentVariables,
};
const paginationVariables = getPaginationVariables(
direction,
count,
cursor,
baseVariables,
{...extraVariables},
paginationMetadata,
);
// If the query needs an identifier value ('id' or similar) and one
// was not explicitly provided, read it from the fragment data.
if (identifierInfo != null) {
// @refetchable fragments are guaranteed to have an `id` selection
// if the type is Node, implements Node, or is @fetchable. Double-check
// that there actually is a value at runtime.
if (typeof identifierValue !== 'string') {
warning(
false,
'Relay: Expected result to have a string ' +
'`%s` in order to refetch, got `%s`.',
identifierInfo.identifierField,
identifierValue,
);
}
paginationVariables[identifierInfo.identifierQueryVariableName] =
identifierValue;
}
const paginationQuery = createOperationDescriptor(
paginationRequest,
paginationVariables,
{force: true},
);
fetchQuery(environment, paginationQuery).subscribe({
...observer,
start: subscription => {
startFetch(subscription);
observer.start && observer.start(subscription);
},
complete: () => {
completeFetch();
observer.complete && observer.complete();
onComplete && onComplete(null);
},
error: error => {
completeFetch();
observer.error && observer.error(error);
onComplete && onComplete(error);
},
});
return {dispose: disposeFetch};
},
// NOTE: We disable react-hooks-deps warning because all values
// inside paginationMetadata are static
// eslint-disable-next-line react-hooks/exhaustive-deps
[
environment,
identifierValue,
direction,
cursor,
startFetch,
disposeFetch,
completeFetch,
isFetchingRef,
isParentQueryActive,
fragmentData,
fragmentNode.name,
fragmentRef,
componentDisplayName,
],
);
return [loadMore, hasMore, disposeFetch];
}
module.exports = useLoadMoreFunction;