UNPKG

react-relay

Version:

A framework for building GraphQL-driven React applications.

281 lines (259 loc) • 8.48 kB
/** * 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 */ 'use strict'; 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;