UNPKG

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