Created
September 19, 2019 21:56
-
-
Save janicduplessis/facf30f35e647af870d43753d8159b87 to your computer and use it in GitHub Desktop.
This file contains 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
/* eslint-disable */ | |
/** | |
* Copyright (c) Facebook, Inc. and its affiliates. | |
* | |
* This source code is licensed under the MIT license found in the | |
* LICENSE file in the root directory of this source tree. | |
* | |
* @flow | |
* @format | |
*/ | |
const React = require('react'); | |
const ReactRelayContext = require('react-relay/lib/ReactRelayContext'); | |
const ReactRelayQueryFetcher = require('react-relay/lib/ReactRelayQueryFetcher'); | |
const areEqual = require('fbjs/lib/areEqual'); | |
const { | |
createOperationDescriptor, | |
deepFreeze, | |
getRequest, | |
} = require('relay-runtime'); | |
/** | |
* React may double-fire the constructor, and we call 'fetch' in the | |
* constructor. If a request is already in flight from a previous call to the | |
* constructor, just reuse the query fetcher and wait for the response. | |
*/ | |
const requestCache = {}; | |
/** | |
* @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) { | |
super(props); | |
// Callbacks are attached to the current instance and shared with static | |
// lifecyles by bundling with state. This is okay to do because the | |
// callbacks don't change in reaction to props. However we should not | |
// "leak" them before mounting (since we would be unable to clean up). For | |
// that reason, we define them as null initially and fill them in after | |
// mounting to avoid leaking memory. | |
const retryCallbacks = { | |
handleDataChange: null, | |
handleRetryAfterError: null, | |
}; | |
let queryFetcher; | |
let requestCacheKey; | |
if (props.query) { | |
const { query } = props; | |
const request = getRequest(query); | |
requestCacheKey = getRequestCacheKey(request.params, props.variables); | |
queryFetcher = requestCache[requestCacheKey] | |
? requestCache[requestCacheKey].queryFetcher | |
: new ReactRelayQueryFetcher(); | |
} else { | |
queryFetcher = new ReactRelayQueryFetcher(); | |
} | |
this.state = { | |
prevPropsEnvironment: props.environment, | |
prevPropsVariables: props.variables, | |
prevQuery: props.query, | |
queryFetcher, | |
retryCallbacks, | |
...fetchQueryAndComputeStateFromProps( | |
props, | |
queryFetcher, | |
{ retryCallbacks }, | |
requestCacheKey, | |
), | |
}; | |
} | |
static getDerivedStateFromProps(nextProps, prevState) { | |
if ( | |
prevState.prevQuery !== nextProps.query || | |
prevState.prevPropsEnvironment !== nextProps.environment || | |
!areEqual(prevState.prevPropsVariables, nextProps.variables) | |
) { | |
const { query } = nextProps; | |
const prevSelectionReferences = prevState.queryFetcher.getSelectionReferences(); | |
prevState.queryFetcher.disposeRequest(); | |
let queryFetcher; | |
if (query) { | |
const request = getRequest(query); | |
const requestCacheKey = getRequestCacheKey( | |
request.params, | |
nextProps.variables, | |
); | |
queryFetcher = requestCache[requestCacheKey] | |
? requestCache[requestCacheKey].queryFetcher | |
: new ReactRelayQueryFetcher(prevSelectionReferences); | |
} else { | |
queryFetcher = new ReactRelayQueryFetcher(prevSelectionReferences); | |
} | |
return { | |
prevQuery: nextProps.query, | |
prevPropsEnvironment: nextProps.environment, | |
prevPropsVariables: nextProps.variables, | |
queryFetcher: queryFetcher, | |
...fetchQueryAndComputeStateFromProps( | |
nextProps, | |
queryFetcher, | |
prevState, | |
// passing no requestCacheKey will cause it to be recalculated internally | |
// and we want the updated requestCacheKey, since variables may have changed | |
), | |
}; | |
} | |
return null; | |
} | |
componentDidMount() { | |
const { retryCallbacks, queryFetcher, requestCacheKey } = this.state; | |
if (requestCacheKey) { | |
delete requestCache[requestCacheKey]; | |
} | |
retryCallbacks.handleDataChange = params => { | |
const error = params.error == null ? null : params.error; | |
const snapshot = params.snapshot == null ? null : params.snapshot; | |
const relayContext = getContext( | |
this.props.environment, | |
this.props.variables, | |
); | |
this.setState(prevState => { | |
const { requestCacheKey: prevRequestCacheKey } = prevState; | |
if (prevRequestCacheKey) { | |
delete requestCache[prevRequestCacheKey]; | |
} | |
// Don't update state if nothing has changed. | |
if (snapshot === prevState.snapshot && error === prevState.error) { | |
return null; | |
} | |
return { | |
renderProps: getRenderProps( | |
error, | |
snapshot, | |
prevState.queryFetcher, | |
prevState.retryCallbacks, | |
), | |
relayContext, | |
snapshot, | |
requestCacheKey: null, | |
}; | |
}); | |
}; | |
retryCallbacks.handleRetryAfterError = error => | |
this.setState(prevState => { | |
const { requestCacheKey: prevRequestCacheKey } = prevState; | |
if (prevRequestCacheKey) { | |
delete requestCache[prevRequestCacheKey]; | |
} | |
return { | |
renderProps: getLoadingRenderProps(), | |
requestCacheKey: null, | |
}; | |
}); | |
// Re-initialize the ReactRelayQueryFetcher with callbacks. | |
// If data has changed since constructions, this will re-render. | |
if (this.props.query) { | |
queryFetcher.setOnDataChange(retryCallbacks.handleDataChange); | |
} | |
} | |
componentDidUpdate(): void { | |
// We don't need to cache the request after the component commits | |
const { requestCacheKey } = this.state; | |
if (requestCacheKey) { | |
delete requestCache[requestCacheKey]; | |
// HACK | |
delete this.state.requestCacheKey; | |
} | |
} | |
componentWillUnmount() { | |
this.state.queryFetcher.dispose(); | |
} | |
shouldComponentUpdate(nextProps, nextState) { | |
return ( | |
nextProps.render !== this.props.render || | |
nextState.renderProps !== this.state.renderProps | |
); | |
} | |
render() { | |
const { renderProps, relayContext } = this.state; | |
// Note that the root fragment results in `renderProps.props` is already | |
// frozen by the store; this call is to freeze the renderProps object and | |
// error property if set. | |
if (__DEV__) { | |
deepFreeze(renderProps); | |
} | |
return ( | |
<ReactRelayContext.Provider value={relayContext}> | |
{this.props.render(renderProps)} | |
</ReactRelayContext.Provider> | |
); | |
} | |
} | |
function getContext(environment, variables) { | |
return { | |
environment, | |
variables, | |
}; | |
} | |
function getLoadingRenderProps() { | |
return { | |
error: null, | |
props: null, // `props: null` indicates that the data is being fetched (i.e. loading) | |
retry: null, | |
stale: false, | |
}; | |
} | |
function getEmptyRenderProps() { | |
return { | |
error: null, | |
props: {}, // `props: {}` indicates no data available | |
retry: null, | |
stale: false, | |
}; | |
} | |
function getRenderProps(error, snapshot, queryFetcher, retryCallbacks) { | |
return { | |
error: error ? error : null, | |
props: snapshot ? snapshot.data : null, | |
retry: cacheConfigOverride => { | |
const syncSnapshot = queryFetcher.retry(cacheConfigOverride); | |
if ( | |
syncSnapshot && | |
typeof retryCallbacks.handleDataChange === 'function' | |
) { | |
retryCallbacks.handleDataChange({ snapshot: syncSnapshot }); | |
} else if ( | |
error && | |
typeof retryCallbacks.handleRetryAfterError === 'function' | |
) { | |
// If retrying after an error and no synchronous result available, | |
// reset the render props | |
retryCallbacks.handleRetryAfterError(error); | |
} | |
}, | |
stale: false, | |
}; | |
} | |
function getRequestCacheKey(request, variables) { | |
const requestID = request.id || request.text; | |
return JSON.stringify({ | |
id: String(requestID), | |
variables, | |
}); | |
} | |
function fetchQueryAndComputeStateFromProps( | |
props, | |
queryFetcher, | |
prevState, | |
requestCacheKey, | |
) { | |
const { environment, query, variables } = props; | |
const { retryCallbacks } = prevState; | |
const genericEnvironment = environment; | |
if (query) { | |
const request = getRequest(query); | |
const operation = createOperationDescriptor(request, variables); | |
const relayContext = getContext( | |
genericEnvironment, | |
operation.request.variables, | |
); | |
if (typeof requestCacheKey === 'string' && requestCache[requestCacheKey]) { | |
// This same request is already in flight. | |
const { snapshot } = requestCache[requestCacheKey]; | |
if (snapshot) { | |
// Use the cached response | |
return { | |
error: null, | |
relayContext, | |
renderProps: getRenderProps( | |
null, | |
snapshot, | |
queryFetcher, | |
retryCallbacks, | |
), | |
snapshot, | |
requestCacheKey, | |
}; | |
} else { | |
// Render loading state | |
return { | |
error: null, | |
relayContext, | |
renderProps: getLoadingRenderProps(), | |
snapshot: null, | |
requestCacheKey, | |
}; | |
} | |
} | |
try { | |
const storeSnapshot = queryFetcher.lookupInStore( | |
genericEnvironment, | |
operation, | |
props.fetchPolicy, | |
); | |
const querySnapshot = queryFetcher.fetch({ | |
cacheConfig: props.cacheConfig, | |
environment: genericEnvironment, | |
onDataChange: retryCallbacks.handleDataChange, | |
operation, | |
}); | |
// Use network data first, since it may be fresher | |
const snapshot = querySnapshot || storeSnapshot; | |
const renderStaleVariables = | |
props.renderStaleVariables && | |
prevState.snapshot !== null && | |
prevState.prevPropsEnvironment === environment; | |
// cache the request to avoid duplicate requests | |
requestCacheKey = | |
requestCacheKey || getRequestCacheKey(request.params, props.variables); | |
requestCache[requestCacheKey] = { queryFetcher, snapshot }; | |
if (!snapshot) { | |
// Keeps rendering old content until new data is available. | |
if (renderStaleVariables) { | |
return { | |
renderProps: { ...prevState.renderProps, stale: true }, | |
requestCacheKey, | |
}; | |
} | |
return { | |
error: null, | |
relayContext, | |
renderProps: getLoadingRenderProps(), | |
snapshot: null, | |
requestCacheKey, | |
}; | |
} | |
return { | |
error: null, | |
relayContext, | |
renderProps: getRenderProps( | |
null, | |
snapshot, | |
queryFetcher, | |
retryCallbacks, | |
), | |
snapshot, | |
requestCacheKey, | |
}; | |
} catch (error) { | |
return { | |
error, | |
relayContext, | |
renderProps: getRenderProps(error, null, queryFetcher, retryCallbacks), | |
snapshot: null, | |
requestCacheKey, | |
}; | |
} | |
} else { | |
queryFetcher.dispose(); | |
const relayContext = getContext(genericEnvironment, variables); | |
return { | |
error: null, | |
relayContext, | |
renderProps: getEmptyRenderProps(), | |
requestCacheKey: null, // if there is an error, don't cache request | |
}; | |
} | |
} | |
module.exports = { QueryRenderer: ReactRelayQueryRenderer }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment