Skip to content

Instantly share code, notes, and snippets.

@janicduplessis
Created September 19, 2019 21:56
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 janicduplessis/facf30f35e647af870d43753d8159b87 to your computer and use it in GitHub Desktop.
Save janicduplessis/facf30f35e647af870d43753d8159b87 to your computer and use it in GitHub Desktop.
/* 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