UNPKG

react-relay

Version:

A framework for building data-driven React applications.

758 lines (657 loc) • 33.1 kB
/** * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule RelayContainer * @typechecks * */ 'use strict'; var _inherits = require('babel-runtime/helpers/inherits')['default']; var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default']; var _extends = require('babel-runtime/helpers/extends')['default']; var _Object$keys = require('babel-runtime/core-js/object/keys')['default']; Object.defineProperty(exports, '__esModule', { value: true }); var ErrorUtils = require('fbjs/lib/ErrorUtils'); var GraphQLDeferredQueryTracker = require('./GraphQLDeferredQueryTracker'); var GraphQLFragmentPointer = require('./GraphQLFragmentPointer'); var GraphQLStoreChangeEmitter = require('./GraphQLStoreChangeEmitter'); var GraphQLStoreDataHandler = require('./GraphQLStoreDataHandler'); var GraphQLStoreQueryResolver = require('./GraphQLStoreQueryResolver'); var React = require('react'); var RelayContainerComparators = require('./RelayContainerComparators'); var RelayContainerProxy = require('./RelayContainerProxy'); var RelayDeprecated = require('./RelayDeprecated'); var RelayFragmentReference = require('./RelayFragmentReference'); var RelayMetaRoute = require('./RelayMetaRoute'); var RelayMutationTransaction = require('./RelayMutationTransaction'); var RelayPendingQueryTracker = require('./RelayPendingQueryTracker'); var RelayPropTypes = require('./RelayPropTypes'); var RelayProfiler = require('./RelayProfiler'); var RelayQuery = require('./RelayQuery'); var RelayStore = require('./RelayStore'); var RelayStoreData = require('./RelayStoreData'); var buildRQL = require('./buildRQL'); var forEachObject = require('fbjs/lib/forEachObject'); var invariant = require('fbjs/lib/invariant'); var nullthrows = require('fbjs/lib/nullthrows'); var prepareRelayContainerProps = require('./prepareRelayContainerProps'); var shallowEqual = require('fbjs/lib/shallowEqual'); var warning = require('fbjs/lib/warning'); GraphQLStoreChangeEmitter.injectBatchingStrategy(React /* #7887700 */.unstable_batchedUpdates); var containerContextTypes = { route: RelayPropTypes.QueryConfig.isRequired }; var nextContainerID = 0; var storeData = RelayStoreData.getDefaultInstance(); /** * @public * * RelayContainer is a higher order component that provides the ability to: * * - Encode data dependencies using query fragments that are parameterized by * routes and variables. * - Manipulate variables via methods on `this.props.relay`. * - Automatically subscribe to data changes. * - Avoid unnecessary updates if data is unchanged. * - Propagate the `route` via context (available on `this.props.relay`). * */ function createContainerComponent(Component, spec, containerID) { var componentName = Component.displayName || Component.name; var containerName = 'Relay(' + componentName + ')'; var fragments = spec.fragments; var fragmentNames = _Object$keys(fragments); var initialVariables = spec.initialVariables || {}; var prepareVariables = spec.prepareVariables; var RelayContainer = (function (_React$Component) { _inherits(RelayContainer, _React$Component); function RelayContainer(props, context) { _classCallCheck(this, RelayContainer); _React$Component.call(this, props, context); var route = context.route; !(route && typeof route.name === 'string') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer: `%s` was rendered without a valid route. Make sure ' + 'the route is valid, and make sure that it is correctly set on the ' + 'parent component\'s context (e.g. using <RelayRootContainer>).', containerName) : invariant(false) : undefined; var self = this; self.forceFetch = this.forceFetch.bind(this); self.getFragmentError = this.getFragmentError.bind(this); self.getPendingTransactions = this.getPendingTransactions.bind(this); self.hasFragmentData = this.hasFragmentData.bind(this); self.hasOptimisticUpdate = this.hasOptimisticUpdate.bind(this); self.setVariables = this.setVariables.bind(this); this._deferredErrors = null; this._deferredSubscriptions = null; this._didShowFakeDataWarning = false; this._fragmentPointers = {}; this._hasNewDeferredData = false; this._hasStaleQueryData = false; this._queryResolvers = {}; this.mounted = true; this.pending = null; this.state = { variables: {}, queryData: {} }; } /** * Requests an update to variables. This primes the cache for the new * variables and notifies the caller of changes via the callback. As data * becomes ready, the component will be updated. */ RelayContainer.prototype.setVariables = function setVariables(partialVariables, callback) { this._runVariables(partialVariables, callback, false); }; /** * Requests an update to variables. Unlike `setVariables`, this forces data * to be fetched and written for the supplied variables. Any data that * previously satisfied the queries will be overwritten. */ RelayContainer.prototype.forceFetch = function forceFetch(partialVariables, callback) { this._runVariables(partialVariables, callback, true); }; /** * Creates a query for each of the component's fragments using the given * variables, and fragment pointers that can be used to resolve the results * of those queries. The fragment pointers are of the same shape as the * `_fragmentPointers` property. */ RelayContainer.prototype._createQuerySetAndFragmentPointers = function _createQuerySetAndFragmentPointers(variables) { var _this = this; var fragmentPointers = {}; var querySet = {}; fragmentNames.forEach(function (fragmentName) { var fragment = getFragment(fragmentName, _this.context.route, variables); var queryData = _this.state.queryData[fragmentName]; if (!fragment || queryData == null) { return; } var fragmentPointer; if (fragment.isPlural()) { !Array.isArray(queryData) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer: Invalid queryData for `%s`, expected an array ' + 'of records because the corresponding fragment is plural.', fragmentName) : invariant(false) : undefined; var dataIDs = []; queryData.forEach(function (data, ii) { var dataID = GraphQLStoreDataHandler.getID(data); if (dataID) { querySet[fragmentName + ii] = storeData.buildFragmentQueryForDataID(fragment, dataID); dataIDs.push(dataID); } }); if (dataIDs.length) { fragmentPointer = new GraphQLFragmentPointer(dataIDs, fragment); } } else { var dataID = GraphQLStoreDataHandler.getID(queryData); if (dataID) { fragmentPointer = new GraphQLFragmentPointer(dataID, fragment); querySet[fragmentName] = storeData.buildFragmentQueryForDataID(fragment, dataID); } } fragmentPointers[fragmentName] = fragmentPointer; }); return { fragmentPointers: fragmentPointers, querySet: querySet }; }; RelayContainer.prototype._runVariables = function _runVariables(partialVariables, callback, forceFetch) { var _this2 = this; var lastVariables = this.state.variables; var prevVariables = this.pending ? this.pending.variables : lastVariables; var nextVariables = mergeVariables(prevVariables, partialVariables); this.pending && this.pending.request.abort(); var completeProfiler = RelayProfiler.profile('RelayContainer.setVariables', { containerName: containerName, nextVariables: nextVariables }); // If variables changed or we are force-fetching, we need to build a new // set of queries that includes the updated variables. Because the pending // fetch is always canceled, always initiate a new fetch. var querySet = {}; var fragmentPointers = null; if (forceFetch || !shallowEqual(nextVariables, lastVariables)) { var _createQuerySetAndFragmentPointers2 = this._createQuerySetAndFragmentPointers(nextVariables); querySet = _createQuerySetAndFragmentPointers2.querySet; fragmentPointers = _createQuerySetAndFragmentPointers2.fragmentPointers; } var onReadyStateChange = ErrorUtils.guard(function (readyState) { var aborted = readyState.aborted; var done = readyState.done; var error = readyState.error; var ready = readyState.ready; var isComplete = aborted || done || error; if (isComplete && _this2.pending === current) { _this2.pending = null; } var partialState; if (ready && fragmentPointers) { // Only update query data if variables changed. Otherwise, `querySet` // and `fragmentPointers` will be empty, and `nextVariables` will be // equal to `lastVariables`. _this2._fragmentPointers = fragmentPointers; _this2._updateQueryResolvers(); var queryData = _this2._getQueryData(_this2.props); partialState = { variables: nextVariables, queryData: queryData }; } else { partialState = {}; } var mounted = _this2.mounted; if (mounted) { var updateProfiler = RelayProfiler.profile('RelayContainer.update'); React /* #7887700 */.unstable_batchedUpdates(function () { _this2.setState(partialState, function () { updateProfiler.stop(); if (isComplete) { completeProfiler.stop(); } }); if (callback) { callback.call(_this2.refs.component, _extends({}, readyState, { mounted: mounted })); } }); } else { if (callback) { callback(_extends({}, readyState, { mounted: mounted })); } if (isComplete) { completeProfiler.stop(); } } }, 'RelayContainer.onReadyStateChange'); var current = { variables: nextVariables, request: forceFetch ? RelayStore.forceFetch(querySet, onReadyStateChange) : RelayStore.primeCache(querySet, onReadyStateChange) }; this.pending = current; }; /** * Determine if the supplied record reflects an optimistic update. */ RelayContainer.prototype.hasOptimisticUpdate = function hasOptimisticUpdate(record) { var dataID = GraphQLStoreDataHandler.getID(record); !(dataID != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer.hasOptimisticUpdate(): Expected a record in `%s`.', componentName) : invariant(false) : undefined; return storeData.getQueuedStore().hasOptimisticUpdate(dataID); }; /** * Returns the pending mutation transactions affecting the given record. */ RelayContainer.prototype.getPendingTransactions = function getPendingTransactions(record) { var dataID = GraphQLStoreDataHandler.getID(record); !(dataID != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer.getPendingTransactions(): Expected a record in `%s`.', componentName) : invariant(false) : undefined; var mutationIDs = storeData.getQueuedStore().getClientMutationIDs(dataID); if (!mutationIDs) { return null; } return mutationIDs.map(RelayMutationTransaction.get); }; /** * Returns any error related to fetching data for a deferred fragment. */ RelayContainer.prototype.getFragmentError = function getFragmentError(fragmentReference, record) { var deferredErrors = this._deferredErrors; if (!deferredErrors) { return null; } var dataID = GraphQLStoreDataHandler.getID(record); if (dataID == null) { // TODO: Throw instead, like we do in `hasFragmentData`, #7857010. process.env.NODE_ENV !== 'production' ? warning(false, 'RelayContainer.getFragmentError(): Invalid call from `%s`. Second ' + 'argument is not a valid record.', componentName) : undefined; return null; } var fragment = RelayQuery.Node.create(fragmentReference.defer(), RelayMetaRoute.get(this.context.route.name), this.state.variables); !(fragment instanceof RelayQuery.Fragment) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer.getFragmentError(): First argument is not a valid ' + 'fragment. Ensure that there are no failing `if` or `unless` ' + 'conditions.') : invariant(false) : undefined; var fragmentID = fragment.getFragmentID(); var subscriptionKey = getSubscriptionKey(dataID, fragmentID); return deferredErrors[subscriptionKey]; }; /** * Checks if data for a deferred fragment is ready. This method should * *always* be called before rendering a child component whose fragment was * deferred (unless that child can handle null or missing data). */ RelayContainer.prototype.hasFragmentData = function hasFragmentData(fragmentReference, record) { if (!RelayPendingQueryTracker.hasPendingQueries() && !this._deferredErrors) { // nothing can be missing => must have data return true; } // convert builder -> fragment in order to get the fragment's name var dataID = GraphQLStoreDataHandler.getID(record); !(dataID != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer.hasFragmentData(): Second argument is not a valid ' + 'record. For `<%s X={this.props.X} />`, use ' + '`this.props.hasFragmentData(%s.getQuery(\'X\'), this.props.X)`.', componentName, componentName) : invariant(false) : undefined; var fragment = RelayQuery.Node.create(fragmentReference.defer(), RelayMetaRoute.get(this.context.route.name), this.state.variables); !(fragment instanceof RelayQuery.Fragment) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer.hasFragmentData(): First argument is not a valid ' + 'fragment. Ensure that there are no failing `if` or `unless` ' + 'conditions.') : invariant(false) : undefined; var fragmentID = fragment.getFragmentID(); var hasData = !GraphQLDeferredQueryTracker.isQueryPending(dataID, fragmentID); var subscriptionKey = getSubscriptionKey(dataID, fragmentID); if (!hasData) { // Query is pending: subscribe for updates to any missing deferred data. var deferredSubscriptions = this._deferredSubscriptions || {}; if (!this._deferredSubscriptions) { this._deferredSubscriptions = deferredSubscriptions; } if (!deferredSubscriptions.hasOwnProperty(subscriptionKey)) { deferredSubscriptions[subscriptionKey] = GraphQLDeferredQueryTracker.addListenerForFragment(dataID, fragmentID, { onSuccess: this._handleDeferredSuccess.bind(this), onFailure: this._handleDeferredFailure.bind(this) }); } } else { // query completed: check for errors if (this._deferredErrors && this._deferredErrors.hasOwnProperty(subscriptionKey)) { hasData = false; } } return hasData; }; RelayContainer.prototype.componentWillMount = function componentWillMount() { var variables = getVariablesWithPropOverrides(spec, this.props, initialVariables); this._updateFragmentPointers(this.props, this.context.route, variables); this._updateQueryResolvers(); var queryData = this._getQueryData(this.props); this.setState({ queryData: queryData, variables: variables }); }; RelayContainer.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps, nextContext) { var _this3 = this; var _nullthrows = nullthrows(nextContext); var route = _nullthrows.route; this.setState(function (state) { var variables = getVariablesWithPropOverrides(spec, nextProps, resetPropOverridesForVariables(spec, nextProps, state.variables)); _this3._updateFragmentPointers(nextProps, route, variables); _this3._updateQueryResolvers(); return { variables: variables, queryData: _this3._getQueryData(nextProps) }; }); }; RelayContainer.prototype.componentWillUnmount = function componentWillUnmount() { // A guarded error in mounting might prevent initialization of resolvers. if (this._queryResolvers) { forEachObject(this._queryResolvers, function (queryResolver) { return queryResolver && queryResolver.reset(); }); } // Remove any subscriptions for pending deferred queries. var deferredSubscriptions = this._deferredSubscriptions; if (deferredSubscriptions) { forEachObject(deferredSubscriptions, function (subscription) { subscription && subscription.remove(); }); } this._deferredErrors = null; this._deferredSubscriptions = null; this._fragmentPointers = {}; this._queryResolvers = {}; var pending = this.pending; if (pending) { pending.request.abort(); this.pending = null; } this.mounted = false; }; RelayContainer.prototype._updateQueryResolvers = function _updateQueryResolvers() { var _this4 = this; var fragmentPointers = this._fragmentPointers; var queryResolvers = this._queryResolvers; fragmentNames.forEach(function (fragmentName) { var fragmentPointer = fragmentPointers[fragmentName]; var queryResolver = queryResolvers[fragmentName]; if (!fragmentPointer) { if (queryResolver) { queryResolver.reset(); queryResolvers[fragmentName] = null; } } else if (!queryResolver) { queryResolver = new GraphQLStoreQueryResolver(fragmentPointer, _this4._handleFragmentDataUpdate.bind(_this4)); queryResolvers[fragmentName] = queryResolver; } }); }; RelayContainer.prototype._handleFragmentDataUpdate = function _handleFragmentDataUpdate() { var queryData = this._getQueryData(this.props); var updateProfiler = RelayProfiler.profile('RelayContainer.handleFragmentDataUpdate'); this.setState({ queryData: queryData }, updateProfiler.stop); }; RelayContainer.prototype._updateFragmentPointers = function _updateFragmentPointers(props, route, variables) { var _this5 = this; var fragmentPointers = this._fragmentPointers; fragmentNames.forEach(function (fragmentName) { var propValue = props[fragmentName]; process.env.NODE_ENV !== 'production' ? warning(propValue !== undefined, 'RelayContainer: Expected query `%s` to be supplied to `%s` as ' + 'a prop from the parent. Pass an explicit `null` if this is ' + 'intentional.', fragmentName, componentName) : undefined; if (!propValue) { fragmentPointers[fragmentName] = null; return; } var fragment = getFragment(fragmentName, route, variables); var concreteFragmentID = fragment.getConcreteFragmentID(); var dataIDOrIDs; if (fragment.isPlural()) { // Plural fragments require the prop value to be an array of fragment // pointers, which are merged into a single fragment pointer to pass // to the query resolver `resolve`. !Array.isArray(propValue) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer: Invalid prop `%s` supplied to `%s`, expected an ' + 'array of records because the corresponding fragment is plural.', fragmentName, componentName) : invariant(false) : undefined; if (propValue.length) { dataIDOrIDs = propValue.reduce(function (acc, item, ii) { var eachFragmentPointer = item[concreteFragmentID]; !eachFragmentPointer ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer: Invalid prop `%s` supplied to `%s`, ' + 'expected element at index %s to have query data.', fragmentName, componentName, ii) : invariant(false) : undefined; return acc.concat(eachFragmentPointer.getDataIDs()); }, []); } else { // An empty plural fragment cannot be observed; the empty array prop // can be passed as-is to the component. dataIDOrIDs = null; } } else { !!Array.isArray(propValue) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer: Invalid prop `%s` supplied to `%s`, expected a ' + 'single record because the corresponding fragment is not plural.', fragmentName, componentName) : invariant(false) : undefined; var fragmentPointer = propValue[concreteFragmentID]; if (fragmentPointer) { dataIDOrIDs = fragmentPointer.getDataID(); } else { // TODO: Throw when we have mock data validation, #6332949. dataIDOrIDs = null; if (process.env.NODE_ENV !== 'production') { if (!_this5._didShowFakeDataWarning) { _this5._didShowFakeDataWarning = true; process.env.NODE_ENV !== 'production' ? warning(false, 'RelayContainer: Expected prop `%s` supplied to `%s` to ' + 'be data fetched by Relay. This is likely an error unless ' + 'you are purposely passing in mock data that conforms to ' + 'the shape of this component\'s fragment.', fragmentName, componentName) : undefined; } } } } fragmentPointers[fragmentName] = dataIDOrIDs ? new GraphQLFragmentPointer(dataIDOrIDs, fragment) : null; }); }; RelayContainer.prototype._getQueryData = function _getQueryData(props) { var _this6 = this; var queryData = {}; var fragmentPointers = this._fragmentPointers; forEachObject(this._queryResolvers, function (queryResolver, propName) { var propValue = props[propName]; var fragmentPointer = fragmentPointers[propName]; if (!propValue || !fragmentPointer) { // Clear any subscriptions since there is no data. queryResolver && queryResolver.reset(); // Allow mock data to pass through without modification. queryData[propName] = propValue; } else { queryData[propName] = queryResolver.resolve(fragmentPointer); } if (_this6.state.queryData.hasOwnProperty(propName) && queryData[propName] !== _this6.state.queryData[propName]) { _this6._hasStaleQueryData = true; } }); return queryData; }; /** * Update query props when deferred data becomes available. */ RelayContainer.prototype._handleDeferredSuccess = function _handleDeferredSuccess(dataID, fragmentID) { var subscriptionKey = getSubscriptionKey(dataID, fragmentID); var deferredSubscriptions = this._deferredSubscriptions; if (deferredSubscriptions && deferredSubscriptions.hasOwnProperty(subscriptionKey)) { // Flag to force `shouldComponentUpdate` to return true. this._hasNewDeferredData = true; deferredSubscriptions[subscriptionKey].remove(); delete deferredSubscriptions[subscriptionKey]; var deferredSuccessProfiler = RelayProfiler.profile('RelayContainer.handleDeferredSuccess'); var queryData = this._getQueryData(this.props); this.setState({ queryData: queryData }, deferredSuccessProfiler.stop); } }; /** * Update query props when deferred queries fail. */ RelayContainer.prototype._handleDeferredFailure = function _handleDeferredFailure(dataID, fragmentID, error) { var subscriptionKey = getSubscriptionKey(dataID, fragmentID); var deferredErrors = this._deferredErrors; if (!deferredErrors) { this._deferredErrors = deferredErrors = {}; } // Flag to force `shouldComponentUpdate` to return true. this._hasNewDeferredData = true; deferredErrors[subscriptionKey] = error; var deferredFailureProfiler = RelayProfiler.profile('RelayContainer.handleDeferredFailure'); // Dummy `setState` to trigger re-render. this.setState(this.state, deferredFailureProfiler.stop); }; RelayContainer.prototype.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState, nextContext) { // TODO: Fix bug with `_hasStaleQueryData` and `_hasNewDeferredData` both // being true. (This will return true two times in a row.) // Flag indicating that query data changed since previous render. if (this._hasStaleQueryData) { this._hasStaleQueryData = false; return true; } // Flag indicating that deferred data has resolved - this component's data // will not change since the data is for a child component, therefore // we force update here. if (this._hasNewDeferredData) { this._hasNewDeferredData = false; return true; } if (this.context.route !== nextContext.route) { return true; } var fragmentPointers = this._fragmentPointers; return !RelayContainerComparators.areNonQueryPropsEqual(fragments, this.props, nextProps) || fragmentPointers && !RelayContainerComparators.areQueryResultsEqual(fragmentPointers, this.state.queryData, nextState.queryData) || !RelayContainerComparators.areQueryVariablesEqual(this.state.variables, nextState.variables); }; RelayContainer.prototype.render = function render() { var relayProps = { forceFetch: this.forceFetch, getFragmentError: this.getFragmentError, getPendingTransactions: this.getPendingTransactions, hasFragmentData: this.hasFragmentData, hasOptimisticUpdate: this.hasOptimisticUpdate, route: this.context.route, setVariables: this.setVariables, variables: this.state.variables }; return React.createElement(Component, _extends({}, this.props, this.state.queryData, prepareRelayContainerProps(relayProps), { ref: 'component' })); }; return RelayContainer; })(React.Component); function getFragment(fragmentName, route, variables) { var fragmentBuilder = fragments[fragmentName]; !fragmentBuilder ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer: Expected `%s` to have a query fragment named `%s`.', containerName, fragmentName) : invariant(false) : undefined; var fragment = buildContainerFragment(containerName, fragmentName, fragmentBuilder, initialVariables); // TODO: Allow routes without names, #7856965. var metaRoute = RelayMetaRoute.get(route.name); if (prepareVariables) { variables = prepareVariables(variables, metaRoute); } return RelayQuery.Node.createFragment(fragment, metaRoute, variables); } initializeProfiler(RelayContainer); RelayContainer.contextTypes = containerContextTypes; RelayContainer.displayName = containerName; RelayContainerProxy.proxyMethods(RelayContainer, Component); return RelayContainer; } /** * TODO: Stop allowing props to override variables, #7856288. */ function getVariablesWithPropOverrides(spec, props, variables) { var initialVariables = spec.initialVariables; if (initialVariables) { var mergedVariables; for (var key in initialVariables) { if (key in props) { mergedVariables = mergedVariables || _extends({}, variables); mergedVariables[key] = props[key]; } } variables = mergedVariables || variables; } return variables; } /** * Compare props and variables and reset the internal query variables if outside * query variables change the component. * * TODO: Stop allowing props to override variables, #7856288. */ function resetPropOverridesForVariables(spec, props, variables) { var initialVariables = spec.initialVariables; for (var key in initialVariables) { if (key in props && props[key] != variables[key]) { return initialVariables; } } return variables; } /** * Constructs a unique key for a deferred subscription. */ function getSubscriptionKey(dataID, fragmentID) { return dataID + '.' + fragmentID; } function initializeProfiler(RelayContainer) { RelayProfiler.instrumentMethods(RelayContainer.prototype, { componentWillMount: 'RelayContainer.prototype.componentWillMount', componentWillReceiveProps: 'RelayContainer.prototype.componentWillReceiveProps', shouldComponentUpdate: 'RelayContainer.prototype.shouldComponentUpdate' }); } /** * Merges a partial update into a set of variables. If no variables changed, the * same object is returned. Otherwise, a new object is returned. */ function mergeVariables(currentVariables, partialVariables) { if (partialVariables) { for (var key in partialVariables) { if (currentVariables[key] !== partialVariables[key]) { return _extends({}, currentVariables, partialVariables); } } } return currentVariables; } /** * Wrapper around `buildRQL.Fragment` with contextual error messages. */ function buildContainerFragment(containerName, fragmentName, fragmentBuilder, variables) { var fragment = buildRQL.Fragment(fragmentBuilder, _Object$keys(variables)); !fragment ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Relay.QL defined on container `%s` named `%s` is not a valid fragment. ' + 'A typical fragment is defined using: Relay.QL`fragment on Type {...}`', containerName, fragmentName) : invariant(false) : undefined; return fragment; } /** * Creates a lazy Relay container. The actual container is created the first * time a container is being constructed by React's rendering engine. */ function create(Component, maybeSpec // spec: RelayContainerSpec ) { var spec = RelayDeprecated.upgradeContainerSpec(maybeSpec); var componentName = Component.displayName || Component.name; var containerName = 'Relay(' + componentName + ')'; var containerID = (nextContainerID++).toString(36); var fragments = spec.fragments; !(typeof fragments === 'object' && fragments) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Relay.createContainer(%s, ...): Missing `fragments`, which is expected ' + 'to be an object mapping from `propName` to: () => Relay.QL`...`', componentName) : invariant(false) : undefined; var fragmentNames = _Object$keys(fragments); var initialVariables = spec.initialVariables || {}; var prepareVariables = spec.prepareVariables; var Container; function ContainerConstructor(props, context) { if (!Container) { Container = createContainerComponent(Component, spec, containerID); } return new Container(props, context); } ContainerConstructor.getFragmentNames = function () { return fragmentNames; }; ContainerConstructor.getQueryNames = RelayDeprecated.createWarning({ was: componentName + '.getQueryNames', now: componentName + '.getFragmentNames', adapter: ContainerConstructor.getFragmentNames }); /** * Retrieves a reference to the fragment by name. An optional second argument * can be supplied to override the component's default variables. */ ContainerConstructor.getFragment = function (fragmentName, variableMapping) { var fragmentBuilder = fragments[fragmentName]; if (!fragmentBuilder) { !false ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s.getFragment(): `%s` is not a valid fragment name. Available ' + 'fragments names: %s', containerName, fragmentName, fragmentNames.map(function (name) { return '`' + name + '`'; }).join(', ')) : invariant(false) : undefined; } !(typeof fragmentBuilder === 'function') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayContainer: Expected `%s.fragments.%s` to be a function returning ' + 'a fragment. Example: `%s: () => Relay.QL`fragment on ...`', containerName, fragmentName, fragmentName) : invariant(false) : undefined; return new RelayFragmentReference(function () { return buildContainerFragment(containerName, fragmentName, fragmentBuilder, initialVariables); }, initialVariables, variableMapping, prepareVariables); }; ContainerConstructor.getQuery = RelayDeprecated.createWarning({ was: componentName + '.getQuery', now: componentName + '.getFragment', adapter: ContainerConstructor.getFragment }); ContainerConstructor.contextTypes = containerContextTypes; ContainerConstructor.displayName = containerName; ContainerConstructor.moduleName = null; return ContainerConstructor; } module.exports = { create: create };