UNPKG

11.6 kBJavaScriptView Raw
1import { __assign } from "tslib";
2import { invariant } from "../../utilities/globals/index.js";
3import * as React from "rehackt";
4import { equal } from "@wry/equality";
5import { DocumentType, verifyDocumentType } from "../parser/index.js";
6import { ApolloError, Observable } from "../../core/index.js";
7import { useApolloClient } from "./useApolloClient.js";
8import { useDeepMemo } from "./internal/useDeepMemo.js";
9import { useSyncExternalStore } from "./useSyncExternalStore.js";
10import { toApolloError } from "./useQuery.js";
11import { useIsomorphicLayoutEffect } from "./internal/useIsomorphicLayoutEffect.js";
12/**
13 * > Refer to the [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/) section for a more in-depth overview of `useSubscription`.
14 *
15 * @example
16 * ```jsx
17 * const COMMENTS_SUBSCRIPTION = gql`
18 * subscription OnCommentAdded($repoFullName: String!) {
19 * commentAdded(repoFullName: $repoFullName) {
20 * id
21 * content
22 * }
23 * }
24 * `;
25 *
26 * function DontReadTheComments({ repoFullName }) {
27 * const {
28 * data: { commentAdded },
29 * loading,
30 * } = useSubscription(COMMENTS_SUBSCRIPTION, { variables: { repoFullName } });
31 * return <h4>New comment: {!loading && commentAdded.content}</h4>;
32 * }
33 * ```
34 * @remarks
35 * #### Consider using `onData` instead of `useEffect`
36 *
37 * If you want to react to incoming data, please use the `onData` option instead of `useEffect`.
38 * 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.
39 * State updates made in an event handler like `onData` might - depending on the React version - be batched and cause only a single rerender.
40 *
41 * Consider the following component:
42 *
43 * ```jsx
44 * export function Subscriptions() {
45 * const { data, error, loading } = useSubscription(query);
46 * const [accumulatedData, setAccumulatedData] = useState([]);
47 *
48 * useEffect(() => {
49 * setAccumulatedData((prev) => [...prev, data]);
50 * }, [data]);
51 *
52 * return (
53 * <>
54 * {loading && <p>Loading...</p>}
55 * {JSON.stringify(accumulatedData, undefined, 2)}
56 * </>
57 * );
58 * }
59 * ```
60 *
61 * Instead of using `useEffect` here, we can re-write this component to use the `onData` callback function accepted in `useSubscription`'s `options` object:
62 *
63 * ```jsx
64 * export function Subscriptions() {
65 * const [accumulatedData, setAccumulatedData] = useState([]);
66 * const { data, error, loading } = useSubscription(
67 * query,
68 * {
69 * onData({ data }) {
70 * setAccumulatedData((prev) => [...prev, data])
71 * }
72 * }
73 * );
74 *
75 * return (
76 * <>
77 * {loading && <p>Loading...</p>}
78 * {JSON.stringify(accumulatedData, undefined, 2)}
79 * </>
80 * );
81 * }
82 * ```
83 *
84 * > ⚠️ **Note:** The `useSubscription` option `onData` is available in Apollo Client >= 3.7. In previous versions, the equivalent option is named `onSubscriptionData`.
85 *
86 * 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`.
87 *
88 * @since 3.0.0
89 * @param subscription - A GraphQL subscription document parsed into an AST by `gql`.
90 * @param options - Options to control how the subscription is executed.
91 * @returns Query result object
92 */
93export function useSubscription(subscription, options) {
94 if (options === void 0) { options = Object.create(null); }
95 var hasIssuedDeprecationWarningRef = React.useRef(false);
96 var client = useApolloClient(options.client);
97 verifyDocumentType(subscription, DocumentType.Subscription);
98 if (!hasIssuedDeprecationWarningRef.current) {
99 hasIssuedDeprecationWarningRef.current = true;
100 if (options.onSubscriptionData) {
101 globalThis.__DEV__ !== false && invariant.warn(options.onData ? 53 : 54);
102 }
103 if (options.onSubscriptionComplete) {
104 globalThis.__DEV__ !== false && invariant.warn(options.onComplete ? 55 : 56);
105 }
106 }
107 var skip = options.skip, fetchPolicy = options.fetchPolicy, errorPolicy = options.errorPolicy, shouldResubscribe = options.shouldResubscribe, context = options.context, extensions = options.extensions, ignoreResults = options.ignoreResults;
108 var variables = useDeepMemo(function () { return options.variables; }, [options.variables]);
109 var recreate = function () {
110 return createSubscription(client, subscription, variables, fetchPolicy, errorPolicy, context, extensions);
111 };
112 var _a = React.useState(options.skip ? null : recreate), observable = _a[0], setObservable = _a[1];
113 var recreateRef = React.useRef(recreate);
114 useIsomorphicLayoutEffect(function () {
115 recreateRef.current = recreate;
116 });
117 if (skip) {
118 if (observable) {
119 setObservable((observable = null));
120 }
121 }
122 else if (!observable ||
123 ((client !== observable.__.client ||
124 subscription !== observable.__.query ||
125 fetchPolicy !== observable.__.fetchPolicy ||
126 errorPolicy !== observable.__.errorPolicy ||
127 !equal(variables, observable.__.variables)) &&
128 (typeof shouldResubscribe === "function" ?
129 !!shouldResubscribe(options)
130 : shouldResubscribe) !== false)) {
131 setObservable((observable = recreate()));
132 }
133 var optionsRef = React.useRef(options);
134 React.useEffect(function () {
135 optionsRef.current = options;
136 });
137 var fallbackLoading = !skip && !ignoreResults;
138 var fallbackResult = React.useMemo(function () { return ({
139 loading: fallbackLoading,
140 error: void 0,
141 data: void 0,
142 variables: variables,
143 }); }, [fallbackLoading, variables]);
144 var ignoreResultsRef = React.useRef(ignoreResults);
145 useIsomorphicLayoutEffect(function () {
146 // We cannot reference `ignoreResults` directly in the effect below
147 // it would add a dependency to the `useEffect` deps array, which means the
148 // subscription would be recreated if `ignoreResults` changes
149 // As a result, on resubscription, the last result would be re-delivered,
150 // rendering the component one additional time, and re-triggering `onData`.
151 // The same applies to `fetchPolicy`, which results in a new `observable`
152 // being created. We cannot really avoid it in that case, but we can at least
153 // avoid it for `ignoreResults`.
154 ignoreResultsRef.current = ignoreResults;
155 });
156 var ret = useSyncExternalStore(React.useCallback(function (update) {
157 if (!observable) {
158 return function () { };
159 }
160 var subscriptionStopped = false;
161 var variables = observable.__.variables;
162 var client = observable.__.client;
163 var subscription = observable.subscribe({
164 next: function (fetchResult) {
165 var _a, _b;
166 if (subscriptionStopped) {
167 return;
168 }
169 var result = {
170 loading: false,
171 // TODO: fetchResult.data can be null but SubscriptionResult.data
172 // expects TData | undefined only
173 data: fetchResult.data,
174 error: toApolloError(fetchResult),
175 variables: variables,
176 };
177 observable.__.setResult(result);
178 if (!ignoreResultsRef.current)
179 update();
180 if (result.error) {
181 (_b = (_a = optionsRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, result.error);
182 }
183 else if (optionsRef.current.onData) {
184 optionsRef.current.onData({
185 client: client,
186 data: result,
187 });
188 }
189 else if (optionsRef.current.onSubscriptionData) {
190 optionsRef.current.onSubscriptionData({
191 client: client,
192 subscriptionData: result,
193 });
194 }
195 },
196 error: function (error) {
197 var _a, _b;
198 error =
199 error instanceof ApolloError ? error : (new ApolloError({ protocolErrors: [error] }));
200 if (!subscriptionStopped) {
201 observable.__.setResult({
202 loading: false,
203 data: void 0,
204 error: error,
205 variables: variables,
206 });
207 if (!ignoreResultsRef.current)
208 update();
209 (_b = (_a = optionsRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
210 }
211 },
212 complete: function () {
213 if (!subscriptionStopped) {
214 if (optionsRef.current.onComplete) {
215 optionsRef.current.onComplete();
216 }
217 else if (optionsRef.current.onSubscriptionComplete) {
218 optionsRef.current.onSubscriptionComplete();
219 }
220 }
221 },
222 });
223 return function () {
224 // immediately stop receiving subscription values, but do not unsubscribe
225 // until after a short delay in case another useSubscription hook is
226 // reusing the same underlying observable and is about to subscribe
227 subscriptionStopped = true;
228 setTimeout(function () {
229 subscription.unsubscribe();
230 });
231 };
232 }, [observable]), function () {
233 return observable && !skip && !ignoreResults ?
234 observable.__.result
235 : fallbackResult;
236 }, function () { return fallbackResult; });
237 return React.useMemo(function () { return (__assign(__assign({}, ret), { restart: function () {
238 invariant(!optionsRef.current.skip, 57);
239 setObservable(recreateRef.current());
240 } })); }, [ret]);
241}
242function createSubscription(client, query, variables, fetchPolicy, errorPolicy, context, extensions) {
243 var options = {
244 query: query,
245 variables: variables,
246 fetchPolicy: fetchPolicy,
247 errorPolicy: errorPolicy,
248 context: context,
249 extensions: extensions,
250 };
251 var __ = __assign(__assign({}, options), { client: client, result: {
252 loading: true,
253 data: void 0,
254 error: void 0,
255 variables: variables,
256 }, setResult: function (result) {
257 __.result = result;
258 } });
259 var observable = null;
260 return Object.assign(new Observable(function (observer) {
261 // lazily start the subscription when the first observer subscribes
262 // to get around strict mode
263 if (!observable) {
264 observable = client.subscribe(options);
265 }
266 var sub = observable.subscribe(observer);
267 return function () { return sub.unsubscribe(); };
268 }), {
269 /**
270 * A tracking object to store details about the observable and the latest result of the subscription.
271 */
272 __: __,
273 });
274}
275//# sourceMappingURL=useSubscription.js.map
\No newline at end of file