@apollo/client
Version:
A fully-featured caching GraphQL client.
276 lines • 11.7 kB
JavaScript
import { __assign } from "tslib";
import { invariant } from "../../utilities/globals/index.js";
import * as React from "rehackt";
import { equal } from "@wry/equality";
import { DocumentType, verifyDocumentType } from "../parser/index.js";
import { ApolloError, Observable } from "../../core/index.js";
import { useApolloClient } from "./useApolloClient.js";
import { useDeepMemo } from "./internal/useDeepMemo.js";
import { useSyncExternalStore } from "./useSyncExternalStore.js";
import { toApolloError } from "./useQuery.js";
import { useIsomorphicLayoutEffect } from "./internal/useIsomorphicLayoutEffect.js";
/**
* > Refer to the [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/) section for a more in-depth overview of `useSubscription`.
*
* @example
* ```jsx
* const COMMENTS_SUBSCRIPTION = gql`
* subscription OnCommentAdded($repoFullName: String!) {
* commentAdded(repoFullName: $repoFullName) {
* id
* content
* }
* }
* `;
*
* function DontReadTheComments({ repoFullName }) {
* const {
* data: { commentAdded },
* loading,
* } = useSubscription(COMMENTS_SUBSCRIPTION, { variables: { repoFullName } });
* return <h4>New comment: {!loading && commentAdded.content}</h4>;
* }
* ```
* @remarks
* #### Consider using `onData` instead of `useEffect`
*
* If you want to react to incoming data, please use the `onData` option instead of `useEffect`.
* State updates you make inside a `useEffect` hook might cause additional rerenders, and `useEffect` is mostly meant for side effects of rendering, not as an event handler.
* State updates made in an event handler like `onData` might - depending on the React version - be batched and cause only a single rerender.
*
* Consider the following component:
*
* ```jsx
* export function Subscriptions() {
* const { data, error, loading } = useSubscription(query);
* const [accumulatedData, setAccumulatedData] = useState([]);
*
* useEffect(() => {
* setAccumulatedData((prev) => [...prev, data]);
* }, [data]);
*
* return (
* <>
* {loading && <p>Loading...</p>}
* {JSON.stringify(accumulatedData, undefined, 2)}
* </>
* );
* }
* ```
*
* Instead of using `useEffect` here, we can re-write this component to use the `onData` callback function accepted in `useSubscription`'s `options` object:
*
* ```jsx
* export function Subscriptions() {
* const [accumulatedData, setAccumulatedData] = useState([]);
* const { data, error, loading } = useSubscription(
* query,
* {
* onData({ data }) {
* setAccumulatedData((prev) => [...prev, data])
* }
* }
* );
*
* return (
* <>
* {loading && <p>Loading...</p>}
* {JSON.stringify(accumulatedData, undefined, 2)}
* </>
* );
* }
* ```
*
* > ⚠️ **Note:** The `useSubscription` option `onData` is available in Apollo Client >= 3.7. In previous versions, the equivalent option is named `onSubscriptionData`.
*
* Now, the first message will be added to the `accumulatedData` array since `onData` is called _before_ the component re-renders. React 18 automatic batching is still in effect and results in a single re-render, but with `onData` we can guarantee each message received after the component mounts is added to `accumulatedData`.
*
* @since 3.0.0
* @param subscription - A GraphQL subscription document parsed into an AST by `gql`.
* @param options - Options to control how the subscription is executed.
* @returns Query result object
*/
export function useSubscription(subscription, options) {
if (options === void 0) { options = Object.create(null); }
var hasIssuedDeprecationWarningRef = React.useRef(false);
var client = useApolloClient(options.client);
verifyDocumentType(subscription, DocumentType.Subscription);
if (!hasIssuedDeprecationWarningRef.current) {
hasIssuedDeprecationWarningRef.current = true;
if (options.onSubscriptionData) {
globalThis.__DEV__ !== false && invariant.warn(options.onData ? 53 : 54);
}
if (options.onSubscriptionComplete) {
globalThis.__DEV__ !== false && invariant.warn(options.onComplete ? 55 : 56);
}
}
var skip = options.skip, fetchPolicy = options.fetchPolicy, errorPolicy = options.errorPolicy, shouldResubscribe = options.shouldResubscribe, context = options.context, extensions = options.extensions, ignoreResults = options.ignoreResults;
var variables = useDeepMemo(function () { return options.variables; }, [options.variables]);
var recreate = function () {
return createSubscription(client, subscription, variables, fetchPolicy, errorPolicy, context, extensions);
};
var _a = React.useState(options.skip ? null : recreate), observable = _a[0], setObservable = _a[1];
var recreateRef = React.useRef(recreate);
useIsomorphicLayoutEffect(function () {
recreateRef.current = recreate;
});
if (skip) {
if (observable) {
setObservable((observable = null));
}
}
else if (!observable ||
((client !== observable.__.client ||
subscription !== observable.__.query ||
fetchPolicy !== observable.__.fetchPolicy ||
errorPolicy !== observable.__.errorPolicy ||
!equal(variables, observable.__.variables)) &&
(typeof shouldResubscribe === "function" ?
!!shouldResubscribe(options)
: shouldResubscribe) !== false)) {
setObservable((observable = recreate()));
}
var optionsRef = React.useRef(options);
React.useEffect(function () {
optionsRef.current = options;
});
var fallbackLoading = !skip && !ignoreResults;
var fallbackResult = React.useMemo(function () { return ({
loading: fallbackLoading,
error: void 0,
data: void 0,
variables: variables,
}); }, [fallbackLoading, variables]);
var ignoreResultsRef = React.useRef(ignoreResults);
useIsomorphicLayoutEffect(function () {
// We cannot reference `ignoreResults` directly in the effect below
// it would add a dependency to the `useEffect` deps array, which means the
// subscription would be recreated if `ignoreResults` changes
// As a result, on resubscription, the last result would be re-delivered,
// rendering the component one additional time, and re-triggering `onData`.
// The same applies to `fetchPolicy`, which results in a new `observable`
// being created. We cannot really avoid it in that case, but we can at least
// avoid it for `ignoreResults`.
ignoreResultsRef.current = ignoreResults;
});
var ret = useSyncExternalStore(React.useCallback(function (update) {
if (!observable) {
return function () { };
}
var subscriptionStopped = false;
var variables = observable.__.variables;
var client = observable.__.client;
var subscription = observable.subscribe({
next: function (fetchResult) {
var _a, _b;
if (subscriptionStopped) {
return;
}
var result = {
loading: false,
// TODO: fetchResult.data can be null but SubscriptionResult.data
// expects TData | undefined only
data: fetchResult.data,
error: toApolloError(fetchResult),
variables: variables,
};
observable.__.setResult(result);
if (!ignoreResultsRef.current)
update();
if (result.error) {
(_b = (_a = optionsRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, result.error);
}
else if (optionsRef.current.onData) {
optionsRef.current.onData({
client: client,
data: result,
});
}
else if (optionsRef.current.onSubscriptionData) {
optionsRef.current.onSubscriptionData({
client: client,
subscriptionData: result,
});
}
},
error: function (error) {
var _a, _b;
error =
error instanceof ApolloError ? error : (new ApolloError({ protocolErrors: [error] }));
if (!subscriptionStopped) {
observable.__.setResult({
loading: false,
data: void 0,
error: error,
variables: variables,
});
if (!ignoreResultsRef.current)
update();
(_b = (_a = optionsRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
}
},
complete: function () {
if (!subscriptionStopped) {
if (optionsRef.current.onComplete) {
optionsRef.current.onComplete();
}
else if (optionsRef.current.onSubscriptionComplete) {
optionsRef.current.onSubscriptionComplete();
}
}
},
});
return function () {
// immediately stop receiving subscription values, but do not unsubscribe
// until after a short delay in case another useSubscription hook is
// reusing the same underlying observable and is about to subscribe
subscriptionStopped = true;
setTimeout(function () {
subscription.unsubscribe();
});
};
}, [observable]), function () {
return observable && !skip && !ignoreResults ?
observable.__.result
: fallbackResult;
}, function () { return fallbackResult; });
var restart = React.useCallback(function () {
invariant(!optionsRef.current.skip, 57);
setObservable(recreateRef.current());
}, [optionsRef, recreateRef]);
return React.useMemo(function () { return (__assign(__assign({}, ret), { restart: restart })); }, [ret, restart]);
}
function createSubscription(client, query, variables, fetchPolicy, errorPolicy, context, extensions) {
var options = {
query: query,
variables: variables,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
context: context,
extensions: extensions,
};
var __ = __assign(__assign({}, options), { client: client, result: {
loading: true,
data: void 0,
error: void 0,
variables: variables,
}, setResult: function (result) {
__.result = result;
} });
var observable = null;
return Object.assign(new Observable(function (observer) {
// lazily start the subscription when the first observer subscribes
// to get around strict mode
if (!observable) {
observable = client.subscribe(options);
}
var sub = observable.subscribe(observer);
return function () { return sub.unsubscribe(); };
}), {
/**
* A tracking object to store details about the observable and the latest result of the subscription.
*/
__: __,
});
}
//# sourceMappingURL=useSubscription.js.map