react-relay
Version:
A framework for building data-driven React applications.
299 lines (262 loc) • 9.49 kB
JavaScript
/**
* 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 RelayRootContainer
* @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 GraphQLFragmentPointer = require('./GraphQLFragmentPointer');
var React = require('react');
var RelayDeprecated = require('./RelayDeprecated');
var RelayStore = require('./RelayStore');
var RelayStoreData = require('./RelayStoreData');
var RelayPropTypes = require('./RelayPropTypes');
var StaticContainer = require('react-static-container');
var getRelayQueries = require('./getRelayQueries');
var invariant = require('fbjs/lib/invariant');
var mapObject = require('fbjs/lib/mapObject');
var PropTypes = React.PropTypes;
var storeData = RelayStoreData.getDefaultInstance();
/**
* @public
*
* RelayRootContainer sends requests for data required to render the supplied
* `Component` and `route`. The `Component` must be a container created using
* `Relay.createContainer`.
*
* See the `RelayStore` module for documentation on `onReadyStateChange`.
*
* === Render Callbacks ===
*
* Whenever the RelayRootContainer renders, one of three render callback props
* are invoked depending on whether data is being loaded, can be resolved, or if
* an error is incurred.
*
* ReactDOM.render(
* <RelayRootContainer
* Component={FooComponent}
* route={fooRoute}
* renderLoading={function() {
* return <View>Loading...</View>;
* }}
* renderFetched={function(data) {
* // Must spread `data` into <FooComponent>.
* return <FooComponent {...data} />;
* }}
* renderFailure={function(error) {
* return <View>Error: {error.message}</View>;
* }}
* />,
* ...
* );
*
* If a callback is not supplied, it has a default behavior:
*
* - Without `renderFetched`, `Component` will be rendered with fetched data.
* - Without `renderFailure`, an error will render to null.
* - Without `renderLoading`, the existing view will continue to render. If
* this is the initial mount (with no existing view), renders to null.
*
* In addition, supplying a `renderLoading` that returns undefined has the same
* effect as not supplying the callback. (Usually, an undefined return value is
* an error in React).
*
* === Refs ===
*
* References to elements rendered by any of these callbacks can be obtained by
* using the React `ref` prop. For example:
*
* <FooComponent {...data} ref={handleFooRef} />
*
* function handleFooRef(component) {
* // Invoked when `<FooComponent>` is mounted or unmounted. When mounted,
* // `component` will be the component. When unmounted, `component` will
* // be null.
* }
*
*/
var RelayRootContainer = (function (_React$Component) {
_inherits(RelayRootContainer, _React$Component);
function RelayRootContainer(props, context) {
_classCallCheck(this, RelayRootContainer);
_React$Component.call(this, props, context);
this.mounted = true;
this.state = this._runQueries(this.props);
}
RelayRootContainer.prototype.getChildContext = function getChildContext() {
return { route: this.props.route };
};
/**
* @private
*/
RelayRootContainer.prototype._runQueries = function _runQueries(_ref) {
var _this = this;
var Component = _ref.Component;
var forceFetch = _ref.forceFetch;
var refetchRoute = _ref.refetchRoute;
var route = _ref.route;
var querySet = getRelayQueries(Component, route);
var onReadyStateChange = function onReadyStateChange(readyState) {
if (!_this.mounted) {
_this._handleReadyStateChange(_extends({}, readyState, { mounted: false }));
return;
}
var _state = _this.state;
var fragmentPointers = _state.fragmentPointers;
var pendingRequest = _state.pendingRequest;
if (request !== pendingRequest) {
// Ignore (abort) ready state if we have a new pending request.
return;
}
if (readyState.aborted || readyState.done || readyState.error) {
pendingRequest = null;
}
if (readyState.ready && !fragmentPointers) {
fragmentPointers = mapObject(querySet, function (query) {
return query ? GraphQLFragmentPointer.createForRoot(storeData.getRecordStore(), query) : null;
});
}
_this.setState({
activeComponent: Component,
activeRoute: route,
error: readyState.error,
fragmentPointers: fragmentPointers,
pendingRequest: pendingRequest,
readyState: _extends({}, readyState, { mounted: true }),
fetchState: {
done: readyState.done,
stale: readyState.stale
}
});
};
if (typeof refetchRoute !== 'undefined') {
RelayDeprecated.warn({
was: 'RelayRootContainer.refetchRoute',
now: 'RelayRootContainer.forceFetch'
});
forceFetch = refetchRoute;
}
var request = forceFetch ? RelayStore.forceFetch(querySet, onReadyStateChange) : RelayStore.primeCache(querySet, onReadyStateChange);
return {
activeComponent: null,
activeRoute: null,
error: null,
fragmentPointers: null,
pendingRequest: request,
readyState: null,
fetchState: {
done: false,
stale: false
}
};
};
/**
* Returns whether or not the view should be updated during the current render
* pass. This is false between invoking `Relay.Store.{primeCache,forceFetch}`
* and the first invocation of the `onReadyStateChange` callback.
*
* @private
*/
RelayRootContainer.prototype._shouldUpdate = function _shouldUpdate() {
return this.props.Component === this.state.activeComponent && this.props.route === this.state.activeRoute;
};
/**
* Exposed as the second argument to the `onFailure` prop.
*
* @private
*/
RelayRootContainer.prototype._retry = function _retry() {
!this.state.error ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayRootContainer: Can only invoke `retry` in a failure state.') : invariant(false) : undefined;
this.setState(this._runQueries(this.props));
};
RelayRootContainer.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
if (nextProps.Component !== this.props.Component || nextProps.route !== this.props.route) {
if (this.state.pendingRequest) {
this.state.pendingRequest.abort();
}
this.setState(this._runQueries(nextProps));
}
};
RelayRootContainer.prototype.componentDidUpdate = function componentDidUpdate(prevProps, prevState) {
// `prevState` should exist; the truthy check is for Flow soundness.
var readyState = this.state.readyState;
if (readyState) {
if (!prevState || readyState !== prevState.readyState) {
this._handleReadyStateChange(readyState);
}
}
};
/**
* @private
*/
RelayRootContainer.prototype._handleReadyStateChange = function _handleReadyStateChange(readyState) {
var onReadyStateChange = this.props.onReadyStateChange;
if (onReadyStateChange) {
onReadyStateChange(readyState);
}
};
RelayRootContainer.prototype.componentWillUnmount = function componentWillUnmount() {
if (this.state.pendingRequest) {
this.state.pendingRequest.abort();
}
this.mounted = false;
};
RelayRootContainer.prototype.render = function render() {
var children = null;
var shouldUpdate = this._shouldUpdate();
if (shouldUpdate && this.state.error) {
var renderFailure = this.props.renderFailure;
if (renderFailure) {
children = renderFailure(this.state.error, this._retry.bind(this));
}
} else if (shouldUpdate && this.state.fragmentPointers) {
var renderFetched = this.props.renderFetched;
if (renderFetched) {
children = renderFetched(_extends({}, this.props.route.params, this.state.fragmentPointers), this.state.fetchState);
} else {
var Component = this.props.Component;
children = React.createElement(Component, _extends({}, this.props.route.params, this.state.fragmentPointers));
}
} else {
var renderLoading = this.props.renderLoading;
if (renderLoading) {
children = renderLoading();
} else {
children = undefined;
}
if (children === undefined) {
children = null;
shouldUpdate = false;
}
}
return React.createElement(
StaticContainer,
{ shouldUpdate: shouldUpdate },
children
);
};
return RelayRootContainer;
})(React.Component);
RelayRootContainer.propTypes = {
Component: RelayPropTypes.Container,
forceFetch: PropTypes.bool,
onReadyStateChange: PropTypes.func,
renderFailure: PropTypes.func,
renderFetched: PropTypes.func,
renderLoading: PropTypes.func,
route: RelayPropTypes.QueryConfig.isRequired
};
RelayRootContainer.childContextTypes = {
route: RelayPropTypes.QueryConfig.isRequired
};
module.exports = RelayRootContainer;
// TODO: Deprecate, #6247867.