Created
August 8, 2017 14:04
-
-
Save miracle2k/d8a9136d0613f7142a9b84378d17973c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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