Skip to content

Instantly share code, notes, and snippets.

@miracle2k
Created August 8, 2017 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miracle2k/d8a9136d0613f7142a9b84378d17973c to your computer and use it in GitHub Desktop.
Save miracle2k/d8a9136d0613f7142a9b84378d17973c to your computer and use it in GitHub Desktop.
const React = require('react');
const RelayPropTypes = require('react-relay/lib/RelayPropTypes');
const areEqual = require('fbjs/lib/areEqual');
const deepFreeze = require('react-relay/lib/deepFreeze');
/**
* @public
*
* Orchestrates fetching and rendering data for a single view or view hierarchy:
* - Fetches the query/variables using the given network implementation.
* - Normalizes the response(s) to that query, publishing them to the given
* store.
* - Renders the pending/fail/success states with the provided render function.
* - Subscribes for updates to the root data and re-renders with any changes.
*/
class ReactRelayQueryRenderer extends React.Component {
constructor(props, context) {
super(props, context);
let {query, variables, lookup} = props;
// TODO (#16225453) QueryRenderer works with old and new environment, but
// the flow typing doesn't quite work abstracted.
// $FlowFixMe
const environment = props.environment;
let operation = null;
if (query) {
const {
createOperationSelector,
getOperation,
} = environment.unstable_internal;
query = getOperation(query);
operation = createOperationSelector(query, variables);
variables = operation.variables;
}
this._pendingFetch = null;
this._relayContext = {
environment,
variables,
};
this._rootSubscription = null;
this._selectionReference = null;
if (query) {
this.state = {
readyState: getDefaultState(),
};
} else {
this.state = {
readyState: {
error: null,
props: {},
retry: null,
},
};
}
if (operation) {
if (lookup && environment.check(operation.root)) {
// data is available in the store, render without making any requests
const snapshot = environment.lookup(operation.fragment);
this.state = {
readyState: {
error: null,
props: snapshot.data,
retry: () => {
this._fetch(operation, props.cacheConfig);
},
}
};
this._rootSubscription = environment.subscribe(snapshot, this._onChange);
} else {
const readyState = this._fetch(operation, props.cacheConfig);
if (readyState) {
this.state = {readyState};
}
}
}
}
componentWillReceiveProps(nextProps) {
if (
nextProps.query !== this.props.query ||
nextProps.environment !== this.props.environment ||
!areEqual(nextProps.variables, this.props.variables)
) {
const {query, variables} = nextProps;
// TODO (#16225453) QueryRenderer works with old and new environment, but
// the flow typing doesn't quite work abstracted.
// $FlowFixMe
const environment = nextProps.environment;
if (query) {
const {
createOperationSelector,
getOperation,
} = environment.unstable_internal;
const operation = createOperationSelector(
getOperation(query),
variables,
);
this._relayContext = {
environment,
variables: operation.variables,
};
if (nextProps.lookup && environment.check(operation.root)) {
const snapshot = environment.lookup(operation.fragment);
this.setState({readyState: {
error: null,
props: snapshot.data,
retry: () => {
this._fetch(operation, nextProps.cacheConfig);
},
}});
this._rootSubscription = environment.subscribe(snapshot, this._onChange);
} else {
const readyState = this._fetch(operation, nextProps.cacheConfig);
this.setState({
readyState: readyState || getDefaultState(),
});
}
} else {
this._relayContext = {
environment,
variables,
};
this._release();
this.setState({
readyState: {
error: null,
props: {},
retry: null,
},
});
}
}
}
componentWillUnmount() {
this._release();
}
shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.render !== this.props.render ||
nextState.readyState !== this.state.readyState
);
}
_release() {
if (this._pendingFetch) {
this._pendingFetch.dispose();
this._pendingFetch = null;
}
if (this._rootSubscription) {
this._rootSubscription.dispose();
this._rootSubscription = null;
}
if (this._selectionReference) {
this._selectionReference.dispose();
this._selectionReference = null;
}
}
_fetch(operation, cacheConfig) {
const {environment} = this._relayContext;
// Immediately retain the results of the new query to prevent relevant data
// from being freed. This is not strictly required if all new data is
// fetched in a single step, but is necessary if the network could attempt
// to incrementally load data (ex: multiple query entries or incrementally
// loading records from disk cache).
const nextReference = environment.retain(operation.root);
let readyState = getDefaultState();
let snapshot: ?Snapshot; // results of the root fragment
let isOnNextCalled = false;
let isFunctionReturned = false;
const onCompleted = () => {
this._pendingFetch = null;
};
const onError = error => {
readyState = {
error,
props: null,
retry: () => {
this._fetch(operation, cacheConfig);
},
};
if (this._selectionReference) {
this._selectionReference.dispose();
}
this._pendingFetch = null;
this._selectionReference = nextReference;
this.setState({readyState});
};
const onNext = () => {
// `onNext` can be called multiple times by network layers that support
// data subscriptions. Wait until the first payload to render `props` and
// subscribe for data updates.
if (snapshot) {
return;
}
snapshot = environment.lookup(operation.fragment);
readyState = {
error: null,
props: snapshot.data,
retry: () => {
this._fetch(operation, cacheConfig);
},
};
if (this._selectionReference) {
this._selectionReference.dispose();
}
this._rootSubscription = environment.subscribe(snapshot, this._onChange);
this._selectionReference = nextReference;
// This line should be called only once.
isOnNextCalled = true;
if (isFunctionReturned) {
this.setState({readyState});
}
};
if (this._pendingFetch) {
this._pendingFetch.dispose();
}
if (this._rootSubscription) {
this._rootSubscription.dispose();
}
const request = environment.streamQuery({
cacheConfig,
onCompleted,
onError,
onNext,
operation,
});
this._pendingFetch = {
dispose() {
request.dispose();
nextReference.dispose();
},
};
isFunctionReturned = true;
return isOnNextCalled ? readyState : null;
}
_onChange = (snapshot) => {
this.setState({
readyState: {
...this.state.readyState,
props: snapshot.data,
},
});
};
getChildContext() {
return {
relay: this._relayContext,
};
}
render() {
// Note that the root fragment results in `readyState.props` is already
// frozen by the store; this call is to freeze the readyState object and
// error property if set.
if (process.env.NODE_ENV == 'development') {
deepFreeze(this.state.readyState);
}
return this.props.render(this.state.readyState);
}
}
ReactRelayQueryRenderer.childContextTypes = {
relay: RelayPropTypes.Relay,
};
function getDefaultState() {
return {
error: null,
props: null,
retry: null,
};
}
module.exports = ReactRelayQueryRenderer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment