Skip to content

Instantly share code, notes, and snippets.

@nodkz
Created December 27, 2017 08:21
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nodkz/ee25063925ea51a753c4ace7c165c1ef to your computer and use it in GitHub Desktop.
Save nodkz/ee25063925ea51a753c4ace7c165c1ef to your computer and use it in GitHub Desktop.
Relay.Modern query renderers
function getRelayQueryRenderer(opts, routeProps, clientStores) {
if (!opts.relayQuery) {
return 'Empty Relay Query';
}
const { query, variables, prepareProps } = opts.relayQuery;
const rendererProps = {
environment: clientStores.relayStore,
query,
variables: !variables
? {}
: variables.call ? variables(routeProps, clientStores) : routeProps.match.params,
render: ({ error, props }) => {
if (!opts.component) {
throw new Error(
'You should provide `component` property for RouteSwipeColumn, ' +
`if you use relayQuery property for path '${opts.path || ''}'.`
);
}
if (error) {
return <BrokenPage devMessage={error.message} />;
} else if (props) {
return React.createElement(opts.component, {
...routeProps,
...(opts.componentProps ? opts.componentProps(routeProps, clientStores) : null),
...(prepareProps ? prepareProps(props, routeProps, clientStores) : props),
...(opts.children ? { children: React.createElement(opts.children) } : null),
});
}
return <LoadingPage />;
},
// TODO
// forceFetch={
// (opts.relayForceFetch && isFunction(opts.relayForceFetch)
// ? opts.relayForceFetch(routeProps, clientStores)
// : !!opts.relayForceFetch) || false
// }
};
// __IS_SERVER__ is defined via Webpack DefinePlugin
return __IS_SERVER__ ? (
<RelaySSRQueryRenderer {...rendererProps} />
) : (
// <QueryRenderer {...rendererProps} />
<RelayLookupQueryRenderer lookup {...rendererProps} />
);
}
/* @flow */
/* eslint-disable no-use-before-define, react/no-unused-prop-types */
import * as React from 'react';
import areEqual from 'fbjs/lib/areEqual';
// forked from https://github.com/robrichard/relay-query-lookup-renderer
// import type { CacheConfig, Disposable } from 'RelayCombinedEnvironmentTypes';
// import type { RelayEnvironmentInterface as ClassicEnvironment } from 'RelayEnvironment';
// import type { GraphQLTaggedNode } from 'RelayModernGraphQLTag';
// import type { Environment, OperationSelector, RelayContext, Snapshot } from 'RelayStoreTypes';
// import type { RerunParam, Variables } from 'RelayTypes';
type CacheConfig = any;
type Disposable = any;
type ClassicEnvironment = any;
type GraphQLTaggedNode = any;
type Environment = any;
type OperationSelector = any;
type RelayContext = any;
type Snapshot = any;
type RerunParam = any;
type Variables = any;
export type Props = {
cacheConfig?: ?CacheConfig,
environment: Environment | ClassicEnvironment,
query: ?GraphQLTaggedNode,
render: (readyState: ReadyState) => ?React.Element<any>,
variables: Variables,
rerunParamExperimental?: RerunParam,
};
export type ReadyState = {
error: ?Error,
props: ?Object,
retry: ?() => void,
};
type State = {
readyState: ReadyState,
};
/**
* @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.
*/
export default class ReactRelayQueryRenderer extends React.Component<Props, State> {
_pendingFetch: ?Disposable;
_relayContext: RelayContext;
_rootSubscription: ?Disposable;
_selectionReference: ?Disposable;
static childContextTypes = {
relay: () => {},
};
constructor(props: Props, context: Object) {
super(props, context);
this._pendingFetch = null;
this._rootSubscription = null;
this._selectionReference = null;
this.state = {
readyState: this._fetchForProps(props),
};
}
componentWillReceiveProps(nextProps: Props): void {
if (
nextProps.query !== this.props.query ||
nextProps.environment !== this.props.environment ||
!areEqual(nextProps.variables, this.props.variables)
) {
this.setState({
readyState: this._fetchForProps(nextProps),
});
}
}
componentWillUnmount(): void {
this._release();
}
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
return nextProps.render !== this.props.render || nextState.readyState !== this.state.readyState;
}
_release(): void {
if (this._pendingFetch) {
this._pendingFetch.dispose();
this._pendingFetch = null;
}
if (!this.props.retain && this._rootSubscription) {
this._rootSubscription.dispose();
this._rootSubscription = null;
}
if (!this.props.retain && this._selectionReference) {
this._selectionReference.dispose();
this._selectionReference = null;
}
}
_fetchForProps(props: Props): ReadyState {
// TODO (#16225453) QueryRenderer works with old and new environment, but
// the flow typing doesn't quite work abstracted.
// $FlowFixMe
const environment: Environment = props.environment;
const { query, variables } = props;
if (query) {
const { createOperationSelector, getOperation } = environment.unstable_internal;
const operation = createOperationSelector(getOperation(query), variables);
this._relayContext = {
environment,
variables: operation.variables,
};
if (props.lookup && environment.check(operation.root)) {
// data is available in the store, render without making any requests
const snapshot = environment.lookup(operation.fragment);
return {
error: null,
props: snapshot.data,
retry: () => {
this._fetch(operation, props.cacheConfig);
},
};
}
return this._fetch(operation, props.cacheConfig) || getDefaultState();
}
this._relayContext = {
environment,
variables,
};
this._release();
return {
error: null,
props: {},
retry: null,
};
}
_fetch(operation: OperationSelector, cacheConfig: ?CacheConfig): ?ReadyState {
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 hasSyncResult = false;
let hasFunctionReturned = false;
if (this._pendingFetch) {
this._pendingFetch.dispose();
}
if (this._rootSubscription) {
this._rootSubscription.dispose();
}
const request = environment
.execute({ operation, cacheConfig })
.finally(() => {
this._pendingFetch = null;
})
.subscribe({
next: () => {
// `next` 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: () => {
// Do not reset the default state if refetching after success,
// handling the case where _fetch may return syncronously instead
// of calling setState.
const syncReadyState = this._fetch(operation, cacheConfig);
if (syncReadyState) {
this.setState({ readyState: syncReadyState });
}
},
};
if (this._selectionReference) {
this._selectionReference.dispose();
}
this._rootSubscription = environment.subscribe(snapshot, this._onChange);
this._selectionReference = nextReference;
// This line should be called only once.
hasSyncResult = true;
if (hasFunctionReturned) {
this.setState({ readyState });
}
},
error: error => {
readyState = {
error,
props: null,
retry: () => {
// Return to the default state when retrying after an error,
// handling the case where _fetch may return syncronously instead
// of calling setState.
const syncReadyState = this._fetch(operation, cacheConfig);
this.setState({ readyState: syncReadyState || getDefaultState() });
},
};
if (this._selectionReference) {
this._selectionReference.dispose();
}
this._selectionReference = nextReference;
hasSyncResult = true;
if (hasFunctionReturned) {
this.setState({ readyState });
}
},
});
this._pendingFetch = {
dispose() {
request.unsubscribe();
nextReference.dispose();
},
};
hasFunctionReturned = true;
return hasSyncResult ? readyState : null;
}
_onChange = (snapshot: Snapshot): void => {
this.setState({
readyState: {
...this.state.readyState,
props: snapshot.data,
},
});
};
getChildContext(): Object {
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 (__DEV__) {
// deepFreeze(this.state.readyState);
// }
return this.props.render(this.state.readyState);
}
}
function getDefaultState(): ReadyState {
return {
error: null,
props: null,
retry: null,
};
}
/* @flow */
import * as React from 'react';
import type RelayStore, { RelayQuery } from 'clientStores/RelayStore';
type ReadyState = {|
error: ?{
message: string,
},
props: ?mixed,
retry: ?() => {},
|};
type Props = {
environment: RelayStore,
query: RelayQuery,
variables: Object,
render: (state: ReadyState) => React.Node,
};
type State = {|
readyState: ReadyState,
|};
export default class RelaySSRQueryRenderer extends React.Component<Props, State> {
static childContextTypes = {
relay: () => {},
};
_relayContext: any;
getChildContext() {
const ctx = this._relayContext;
return ctx;
}
state: State = {
readyState: {
error: null,
props: null,
retry: null,
},
};
componentWillMount() {
const { environment, query, variables } = this.props;
const { getOperation, createOperationSelector } = environment.unstable_internal;
const request = getOperation(query);
const operation = createOperationSelector(request, variables || {});
this._relayContext = {
relay: {
environment,
variables: operation.variables || {},
},
};
// take data from Environment
if (environment.check(operation.root)) {
const snapshot = environment.lookup(operation.fragment);
if (snapshot) {
this.setState({
readyState: {
error: null,
props: snapshot.data || null,
retry: null,
},
});
return;
}
}
// take data from Cache (as fallback)
// const queryID = request.id || request.name;
// const res = environment._cache.get(queryID, variables);
// if (res) {
// this.setState({
// readyState: {
// error: res.error || null,
// props: res.data || null,
// retry: null,
// },
// });
// return;
// }
// just run request (React SSR does not work with async methods)
// so serverRenderHtml will wait all current requests,
// and re-run rendering when data will be avaliable in store/cache
environment.fetch({
query,
variables,
});
}
render() {
return this.props.render(this.state.readyState);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment