react-relay
Version:
A framework for building GraphQL-driven React applications.
281 lines (259 loc) • 8.48 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,
Subscription,
Variables,
} from 'relay-runtime';
const getConnectionState = require('./getConnectionState');
const useIsMountedRef = require('./useIsMountedRef');
const useIsOperationNodeActive = require('./useIsOperationNodeActive');
const useRelayEnvironment = require('./useRelayEnvironment');
const invariant = require('invariant');
const {useCallback, useRef, useState} = require('react');
const {
__internal: {fetchQuery},
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_EXPERIMENTAL<TVariables: Variables>(
args: UseLoadMoreFunctionArgs,
): [
// Function to load more data
LoadMoreFn<TVariables>,
// Whether the connection has more data to load
boolean,
// Force dispose function which cancels the in-flight fetch itself, and callbacks
() => void,
] {
const {
direction,
fragmentNode,
fragmentRef,
fragmentIdentifier,
fragmentData,
connectionPathInFragmentData,
paginationRequest,
paginationMetadata,
componentDisplayName,
observer,
onReset,
} = args;
const environment = useRelayEnvironment();
const {identifierInfo} = getRefetchMetadata(
fragmentNode,
componentDisplayName,
);
const identifierValue =
identifierInfo?.identifierField != null &&
fragmentData != null &&
typeof fragmentData === 'object'
? fragmentData[identifierInfo.identifierField]
: null;
const fetchStatusRef = useRef<
{kind: 'fetching', subscription: Subscription} | {kind: 'none'},
>({kind: 'none'});
const [mirroredEnvironment, setMirroredEnvironment] = useState(environment);
const [mirroredFragmentIdentifier, setMirroredFragmentIdentifier] =
useState(fragmentIdentifier);
const isParentQueryActive = useIsOperationNodeActive(
fragmentNode,
fragmentRef,
);
const forceDisposeFn = useCallback(() => {
// $FlowFixMe[react-rule-unsafe-ref]
if (fetchStatusRef.current.kind === 'fetching') {
// $FlowFixMe[react-rule-unsafe-ref]
fetchStatusRef.current.subscription.unsubscribe();
}
// $FlowFixMe[react-rule-unsafe-ref]
fetchStatusRef.current = {kind: 'none'};
}, []);
const shouldReset =
environment !== mirroredEnvironment ||
fragmentIdentifier !== mirroredFragmentIdentifier;
if (shouldReset) {
forceDisposeFn();
onReset();
setMirroredEnvironment(environment);
setMirroredFragmentIdentifier(fragmentIdentifier);
}
const {cursor, hasMore} = getConnectionState(
direction,
fragmentNode,
fragmentData,
connectionPathInFragmentData,
);
const isMountedRef = useIsMountedRef();
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 (
fetchStatusRef.current.kind === 'fetching' ||
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 => {
fetchStatusRef.current = {kind: 'fetching', subscription};
observer.start && observer.start(subscription);
},
complete: () => {
fetchStatusRef.current = {kind: 'none'};
observer.complete && observer.complete();
onComplete && onComplete(null);
},
error: error => {
fetchStatusRef.current = {kind: 'none'};
observer.complete && observer.complete();
onComplete && onComplete(error);
},
});
return {
dispose: () => {},
};
},
// 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,
isParentQueryActive,
fragmentData,
fragmentNode.name,
fragmentRef,
componentDisplayName,
],
);
return [loadMore, hasMore, forceDisposeFn];
}
module.exports = useLoadMoreFunction_EXPERIMENTAL;