UNPKG

16 kBJavaScriptView Raw
1/**
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 *
8 * @format
9 */
10// flowlint ambiguous-object-type:error
11'use strict';
12
13var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
14
15var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
16
17var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
18
19var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));
20
21var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
22
23var _inheritsLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/inheritsLoose"));
24
25var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
26
27var React = require('react');
28
29var ReactRelayContext = require('./ReactRelayContext');
30
31var ReactRelayQueryFetcher = require('./ReactRelayQueryFetcher');
32
33var areEqual = require("fbjs/lib/areEqual");
34
35var buildReactRelayContainer = require('./buildReactRelayContainer');
36
37var getRootVariablesForFragments = require('./getRootVariablesForFragments');
38
39var warning = require("fbjs/lib/warning");
40
41var _require = require('./ReactRelayContainerUtils'),
42 getContainerName = _require.getContainerName;
43
44var _require2 = require('./RelayContext'),
45 assertRelayContext = _require2.assertRelayContext;
46
47var _require3 = require('relay-runtime'),
48 Observable = _require3.Observable,
49 createFragmentSpecResolver = _require3.createFragmentSpecResolver,
50 createOperationDescriptor = _require3.createOperationDescriptor,
51 getDataIDsFromObject = _require3.getDataIDsFromObject,
52 getRequest = _require3.getRequest,
53 getSelector = _require3.getSelector,
54 getVariablesFromObject = _require3.getVariablesFromObject,
55 isScalarAndEqual = _require3.isScalarAndEqual;
56
57/**
58 * Composes a React component class, returning a new class that intercepts
59 * props, resolving them with the provided fragments and subscribing for
60 * updates.
61 */
62function createContainerWithFragments(Component, fragments, taggedNode) {
63 var _class, _temp;
64
65 var containerName = getContainerName(Component);
66 return _temp = _class = /*#__PURE__*/function (_React$Component) {
67 (0, _inheritsLoose2["default"])(_class, _React$Component);
68
69 function _class(props) {
70 var _this;
71
72 _this = _React$Component.call(this, props) || this;
73 (0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleFragmentDataUpdate", function () {
74 var resolverFromThisUpdate = _this.state.resolver;
75
76 _this.setState(function (updatedState) {
77 return (// If this event belongs to the current data source, update.
78 // Otherwise we should ignore it.
79 resolverFromThisUpdate === updatedState.resolver ? {
80 data: updatedState.resolver.resolve()
81 } : null
82 );
83 });
84 });
85 (0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_refetch", function (refetchVariables, renderVariables, observerOrCallback, options) {
86 if (_this._isUnmounted) {
87 process.env.NODE_ENV !== "production" ? warning(false, 'ReactRelayRefetchContainer: Unexpected call of `refetch` ' + 'on unmounted container `%s`. It looks like some instances ' + 'of your container still trying to refetch the data but they already ' + 'unmounted. Please make sure you clear all timers, intervals, async ' + 'calls, etc that may trigger `refetch`.', containerName) : void 0;
88 return {
89 dispose: function dispose() {}
90 };
91 }
92
93 var _assertRelayContext = assertRelayContext(_this.props.__relayContext),
94 environment = _assertRelayContext.environment;
95
96 var rootVariables = getRootVariablesForFragments(fragments, _this.props);
97 var fetchVariables = typeof refetchVariables === 'function' ? refetchVariables(_this._getFragmentVariables()) : refetchVariables;
98 fetchVariables = (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, rootVariables), fetchVariables);
99 var fragmentVariables = renderVariables ? (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, fetchVariables), renderVariables) : fetchVariables;
100 var cacheConfig = options ? {
101 force: !!options.force
102 } : undefined;
103
104 if (cacheConfig != null && (options === null || options === void 0 ? void 0 : options.metadata) != null) {
105 cacheConfig.metadata = options === null || options === void 0 ? void 0 : options.metadata;
106 }
107
108 var observer = typeof observerOrCallback === 'function' ? {
109 // callback is not exectued on complete or unsubscribe
110 // for backward compatibility
111 next: observerOrCallback,
112 error: observerOrCallback
113 } : observerOrCallback || {};
114 var query = getRequest(taggedNode);
115 var operation = createOperationDescriptor(query, fetchVariables, cacheConfig); // TODO: T26288752 find a better way
116
117 /* eslint-disable lint/react-state-props-mutation */
118
119 _this.state.localVariables = fetchVariables;
120 /* eslint-enable lint/react-state-props-mutation */
121 // Cancel any previously running refetch.
122
123 _this._refetchSubscription && _this._refetchSubscription.unsubscribe(); // Declare refetchSubscription before assigning it in .start(), since
124 // synchronous completion may call callbacks .subscribe() returns.
125
126 var refetchSubscription;
127
128 var storeSnapshot = _this._getQueryFetcher().lookupInStore(environment, operation, options === null || options === void 0 ? void 0 : options.fetchPolicy);
129
130 if (storeSnapshot != null) {
131 _this.state.resolver.setVariables(fragmentVariables, operation.request.node);
132
133 _this.setState(function (latestState) {
134 return {
135 data: latestState.resolver.resolve(),
136 contextForChildren: {
137 environment: _this.props.__relayContext.environment
138 }
139 };
140 }, function () {
141 observer.next && observer.next();
142 observer.complete && observer.complete();
143 });
144
145 return {
146 dispose: function dispose() {}
147 };
148 }
149
150 _this._getQueryFetcher().execute({
151 environment: environment,
152 operation: operation,
153 // TODO (T26430099): Cleanup old references
154 preservePreviousReferences: true
155 }).mergeMap(function (response) {
156 _this.state.resolver.setVariables(fragmentVariables, operation.request.node);
157
158 return Observable.create(function (sink) {
159 return _this.setState(function (latestState) {
160 return {
161 data: latestState.resolver.resolve(),
162 contextForChildren: {
163 environment: _this.props.__relayContext.environment
164 }
165 };
166 }, function () {
167 sink.next();
168 sink.complete();
169 });
170 });
171 })["finally"](function () {
172 // Finalizing a refetch should only clear this._refetchSubscription
173 // if the finizing subscription is the most recent call.
174 if (_this._refetchSubscription === refetchSubscription) {
175 _this._refetchSubscription = null;
176 }
177 }).subscribe((0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, observer), {}, {
178 start: function start(subscription) {
179 _this._refetchSubscription = refetchSubscription = subscription;
180 observer.start && observer.start(subscription);
181 }
182 }));
183
184 return {
185 dispose: function dispose() {
186 refetchSubscription && refetchSubscription.unsubscribe();
187 }
188 };
189 });
190 var relayContext = assertRelayContext(props.__relayContext);
191 _this._refetchSubscription = null; // Do not provide a subscription/callback here.
192 // It is possible for this render to be interrupted or aborted,
193 // In which case the subscription would cause a leak.
194 // We will add the subscription in componentDidMount().
195
196 var resolver = createFragmentSpecResolver(relayContext, containerName, fragments, props);
197 _this.state = {
198 data: resolver.resolve(),
199 localVariables: null,
200 prevProps: props,
201 prevPropsContext: relayContext,
202 contextForChildren: relayContext,
203 relayProp: getRelayProp(relayContext.environment, _this._refetch),
204 resolver: resolver
205 };
206 _this._isUnmounted = false;
207 return _this;
208 }
209
210 var _proto = _class.prototype;
211
212 _proto.componentDidMount = function componentDidMount() {
213 this._subscribeToNewResolver();
214 };
215
216 _proto.componentDidUpdate = function componentDidUpdate(prevProps, prevState) {
217 // If the environment has changed or props point to new records then
218 // previously fetched data and any pending fetches no longer apply:
219 // - Existing references are on the old environment.
220 // - Existing references are based on old variables.
221 // - Pending fetches are for the previous records.
222 if (this.state.resolver !== prevState.resolver) {
223 prevState.resolver.dispose();
224 this._queryFetcher && this._queryFetcher.dispose();
225 this._refetchSubscription && this._refetchSubscription.unsubscribe();
226
227 this._subscribeToNewResolver();
228 }
229 }
230 /**
231 * When new props are received, read data for the new props and add it to
232 * state. Props may be the same in which case previous data can be reused.
233 */
234 ;
235
236 _class.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps, prevState) {
237 // Any props change could impact the query, so we mirror props in state.
238 // This is an unusual pattern, but necessary for this container usecase.
239 var prevProps = prevState.prevProps;
240 var relayContext = assertRelayContext(nextProps.__relayContext);
241 var prevIDs = getDataIDsFromObject(fragments, prevProps);
242 var nextIDs = getDataIDsFromObject(fragments, nextProps);
243 var prevRootVariables = getRootVariablesForFragments(fragments, prevProps);
244 var nextRootVariables = getRootVariablesForFragments(fragments, nextProps);
245 var resolver = prevState.resolver; // If the environment has changed or props point to new records then
246 // previously fetched data and any pending fetches no longer apply:
247 // - Existing references are on the old environment.
248 // - Existing references are based on old variables.
249 // - Pending fetches are for the previous records.
250
251 if (prevState.prevPropsContext.environment !== relayContext.environment || !areEqual(prevRootVariables, nextRootVariables) || !areEqual(prevIDs, nextIDs)) {
252 // Do not provide a subscription/callback here.
253 // It is possible for this render to be interrupted or aborted,
254 // In which case the subscription would cause a leak.
255 // We will add the subscription in componentDidUpdate().
256 resolver = createFragmentSpecResolver(relayContext, containerName, fragments, nextProps);
257 return {
258 data: resolver.resolve(),
259 localVariables: null,
260 prevProps: nextProps,
261 prevPropsContext: relayContext,
262 contextForChildren: relayContext,
263 relayProp: getRelayProp(relayContext.environment, prevState.relayProp.refetch),
264 resolver: resolver
265 };
266 } else if (!prevState.localVariables) {
267 resolver.setProps(nextProps);
268 }
269
270 var data = resolver.resolve();
271
272 if (data !== prevState.data) {
273 return {
274 data: data,
275 prevProps: nextProps
276 };
277 }
278
279 return null;
280 };
281
282 _proto.componentWillUnmount = function componentWillUnmount() {
283 this._isUnmounted = true;
284 this.state.resolver.dispose();
285 this._queryFetcher && this._queryFetcher.dispose();
286 this._refetchSubscription && this._refetchSubscription.unsubscribe();
287 };
288
289 _proto.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) {
290 // Short-circuit if any Relay-related data has changed
291 if (nextState.data !== this.state.data || nextState.relayProp !== this.state.relayProp) {
292 return true;
293 } // Otherwise, for convenience short-circuit if all non-Relay props
294 // are scalar and equal
295
296
297 var keys = Object.keys(nextProps);
298
299 for (var ii = 0; ii < keys.length; ii++) {
300 var _key = keys[ii];
301
302 if (_key === '__relayContext') {
303 if (this.state.prevPropsContext.environment !== nextState.prevPropsContext.environment) {
304 return true;
305 }
306 } else {
307 if (!fragments.hasOwnProperty(_key) && !isScalarAndEqual(nextProps[_key], this.props[_key])) {
308 return true;
309 }
310 }
311 }
312
313 return false;
314 };
315
316 _proto._subscribeToNewResolver = function _subscribeToNewResolver() {
317 var _this$state = this.state,
318 data = _this$state.data,
319 resolver = _this$state.resolver; // Event listeners are only safe to add during the commit phase,
320 // So they won't leak if render is interrupted or errors.
321
322 resolver.setCallback(this._handleFragmentDataUpdate); // External values could change between render and commit.
323 // Check for this case, even though it requires an extra store read.
324
325 var maybeNewData = resolver.resolve();
326
327 if (data !== maybeNewData) {
328 this.setState({
329 data: maybeNewData
330 });
331 }
332 }
333 /**
334 * Render new data for the existing props/context.
335 */
336 ;
337
338 _proto._getFragmentVariables = function _getFragmentVariables() {
339 return getVariablesFromObject(fragments, this.props);
340 };
341
342 _proto._getQueryFetcher = function _getQueryFetcher() {
343 if (!this._queryFetcher) {
344 this._queryFetcher = new ReactRelayQueryFetcher();
345 }
346
347 return this._queryFetcher;
348 };
349
350 _proto.render = function render() {
351 var _this$props = this.props,
352 componentRef = _this$props.componentRef,
353 __relayContext = _this$props.__relayContext,
354 props = (0, _objectWithoutPropertiesLoose2["default"])(_this$props, ["componentRef", "__relayContext"]);
355 var _this$state2 = this.state,
356 relayProp = _this$state2.relayProp,
357 contextForChildren = _this$state2.contextForChildren;
358 return /*#__PURE__*/React.createElement(ReactRelayContext.Provider, {
359 value: contextForChildren
360 }, /*#__PURE__*/React.createElement(Component, (0, _extends2["default"])({}, props, this.state.data, {
361 ref: componentRef,
362 relay: relayProp
363 })));
364 };
365
366 return _class;
367 }(React.Component), (0, _defineProperty2["default"])(_class, "displayName", containerName), _temp;
368}
369
370function getRelayProp(environment, refetch) {
371 return {
372 environment: environment,
373 refetch: refetch
374 };
375}
376/**
377 * Wrap the basic `createContainer()` function with logic to adapt to the
378 * `context.relay.environment` in which it is rendered. Specifically, the
379 * extraction of the environment-specific version of fragments in the
380 * `fragmentSpec` is memoized once per environment, rather than once per
381 * instance of the container constructed/rendered.
382 */
383
384
385function createContainer(Component, fragmentSpec, taggedNode) {
386 // $FlowFixMe[incompatible-return]
387 return buildReactRelayContainer(Component, fragmentSpec, function (ComponentClass, fragments) {
388 return createContainerWithFragments(ComponentClass, fragments, taggedNode);
389 });
390}
391
392module.exports = {
393 createContainer: createContainer
394};
\No newline at end of file