react-relay
Version:
A framework for building GraphQL-driven React applications.
173 lines (161 loc) • 5.65 kB
Flow
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall relay
*/
;
const React = require('react');
const {useMemo} = React;
/**
* Renders the results of a data-driven dependency fetched with the `@match`
* directive. The `@match` directive can be used to specify a mapping of
* result types to the containers used to render those types. The result
* value is an opaque object that described which component was selected
* and a reference to its data. Use <MatchContainer/> to render these
* values.
*
* ## Example
*
* For example, consider a piece of media content that might be text or
* an image, where for clients that don't support images the application
* should fall back to rendering the image caption as text. @match can be
* used to dynamically select whether to render a given media item as
* an image or text (on the server) and then fetch the corresponding
* React component and its data dependencies (information about the
* image or about the text).
*
* ```
* // Media.react.js
*
* // Define a React component that uses <MatchContainer /> to render the
* // results of a @module selection
* function Media(props) {
* const {media, ...restProps} = props;
*
* const loader = moduleReference => {
* // given the data returned by your server for the @module directive,
* // return the React component (or throw a Suspense promise if
* // it is loading asynchronously).
* todo_returnModuleOrThrowPromise(moduleReference);
* };
* return <MatchContainer
* loader={loader}
* match={media.mediaAttachment}
* props={restProps}
* />;
* }
*
* module.exports = createSuspenseFragmentContainer(
* Media,
* {
* media: graphql`
* fragment Media_media on Media {
* # ...
* mediaAttachment @match {
* ...ImageContainer_image @module(name: "ImageContainer.react")
* ...TextContainer_text @module(name: "TextContainer.react")
* }
* }
* `
* },
* );
* ```
*
* ## API
*
* MatchContainer accepts the following props:
* - `match`: The results (an opaque object) of a `@match` field.
* - `props`: Props that should be passed through to the dynamically
* selected component. Note that any of the components listed in
* `@module()` could be selected, so all components should accept
* the value passed here.
* - `loader`: A function to load a module given a reference (whatever
* your server returns for the `js(moduleName: String)` field).
*
*/
// Note: this type is intentionally non-exact, it is expected that the
// object may contain sibling fields.
type TypenameOnlyPointer = {+__typename: string};
export type MatchPointer = {
+__fragmentPropName?: ?string,
+__module_component?: mixed,
+$fragmentSpreads: mixed,
...
};
export type MatchContainerProps<TProps: {...}, TFallback: React.Node> = {
+fallback?: ?TFallback,
+loader: (module: mixed) => component(...TProps),
+match: ?MatchPointer | ?TypenameOnlyPointer,
+props?: TProps,
};
function MatchContainer<TProps: {...}, TFallback: React.Node | null>({
fallback,
loader,
match,
props,
}: MatchContainerProps<TProps, TFallback>):
| React.MixedElement
| TFallback
| null {
if (match != null && typeof match !== 'object') {
throw new Error(
'MatchContainer: Expected `match` value to be an object or null/undefined.',
);
}
// NOTE: the MatchPointer type has a $fragmentSpreads field to ensure that only
// an object that contains a FragmentSpread can be passed. If the fragment
// spread matches, then the metadata fields below (__id, __fragments, etc.)
// will be present. But they can be missing if all the fragment spreads use
// @module and none of the types matched. The cast here is necessary because
// fragment Flow types don't describe metadata fields, only the actual schema
// fields the developer selected.
const {
__id,
__fragments,
__fragmentOwner,
__fragmentPropName,
__module_component,
} = (match: $FlowFixMe) ?? {};
if (
(__fragmentOwner != null && typeof __fragmentOwner !== 'object') ||
(__fragmentPropName != null && typeof __fragmentPropName !== 'string') ||
(__fragments != null && typeof __fragments !== 'object') ||
(__id != null && typeof __id !== 'string')
) {
throw new Error(
"MatchContainer: Invalid 'match' value, expected an object that has a " +
"'...SomeFragment' spread.",
);
}
const LoadedContainer =
__module_component != null ? loader(__module_component) : null;
const fragmentProps = useMemo(() => {
// TODO: Perform this transformation in RelayReader so that unchanged
// output of subscriptions already has a stable identity.
if (__fragmentPropName != null && __id != null && __fragments != null) {
const fragProps: {
[string]: {
__fragmentOwner: $FlowFixMe,
__fragments: $FlowFixMe,
__id: string,
},
} = {};
fragProps[__fragmentPropName] = {__id, __fragments, __fragmentOwner};
return fragProps;
}
return null;
}, [__id, __fragments, __fragmentOwner, __fragmentPropName]);
if (LoadedContainer != null && fragmentProps != null) {
// $FlowFixMe[incompatible-type]
// $FlowFixMe[cannot-spread-indexer]
return <LoadedContainer {...props} {...fragmentProps} />;
} else {
return fallback ?? null;
}
}
module.exports = MatchContainer;