UNPKG

react-ssr-utils

Version:

Several utilities to bootstrap and simplify any React SSR setup

247 lines (211 loc) 8.03 kB
// See: https://github.com/apollographql/react-apollo/blob/8147965/src/getDataFromTree.ts // Modified to accept a Component and rootContext argument, and return a new function that // accepts props. import * as React from 'react'; import { PropsLike } from './types'; import { isReactElement, isComponentClass } from './utils'; export interface Context { [key: string]: any; } interface PromiseTreeArgument { rootElement: React.ReactNode; rootContext?: Context; } interface FetchComponent extends React.Component<any> { fetchData(): Promise<void>; } interface PromiseTreeResult { promise: Promise<any>; context: Context; instance: FetchComponent; } interface PreactElement<P> { attributes: P; } function getProps<P>(element: React.ReactElement<P> | PreactElement<P>): P { return (element as React.ReactElement<P>).props || (element as PreactElement<P>).attributes; } function providesChildContext( instance: React.Component<any>, ): instance is React.Component<any> & React.ChildContextProvider<any> { return !!(instance as any).getChildContext; } // Recurse a React Element tree, running visitor on each element. // If visitor returns `false`, don't call the element's render function // or recurse into its child elements. export function walkTree( element: React.ReactNode, context: Context, visitor: ( element: React.ReactNode, instance: React.Component<any> | null, context: Context, childContext?: Context, ) => boolean | void, ) { if (Array.isArray(element)) { element.forEach(item => walkTree(item, context, visitor)); return; } if (!element) { return; } // A stateless functional component or a class if (isReactElement(element)) { if (typeof element.type === 'function') { const Comp = element.type; const props = Object.assign({}, Comp.defaultProps, getProps(element)); let childContext = context; let child; // Are we are a react class? if (isComponentClass(Comp)) { const instance = new Comp(props, context); // In case the user doesn't pass these to super in the constructor. // Note: `Component.props` are now readonly in `@types/react`, so // we're using `defineProperty` as a workaround (for now). Object.defineProperty(instance, 'props', { value: instance.props || props, }); instance.context = instance.context || context; // Set the instance state to null (not undefined) if not set, to match React behaviour instance.state = instance.state || null; // Override setState to just change the state, not queue up an update // (we can't do the default React thing as we aren't mounted // "properly", however we don't need to re-render as we only support // setState in componentWillMount, which happens *before* render). instance.setState = newState => { if (typeof newState === 'function') { // React's TS type definitions don't contain context as a third parameter for // setState's updater function. // Remove this cast to `any` when that is fixed. newState = (newState as any)(instance.state, instance.props, instance.context); } instance.state = Object.assign({}, instance.state, newState); }; if (Comp.getDerivedStateFromProps) { const result = Comp.getDerivedStateFromProps(instance.props, instance.state); if (result !== null) { instance.state = Object.assign({}, instance.state, result); } } else if (instance.UNSAFE_componentWillMount) { instance.UNSAFE_componentWillMount(); } else if (instance.componentWillMount) { instance.componentWillMount(); } if (providesChildContext(instance)) { childContext = Object.assign({}, context, instance.getChildContext()); } if (visitor(element, instance, context, childContext) === false) { return; } child = instance.render(); } else { // Just a stateless functional if (visitor(element, null, context) === false) { return; } child = Comp(props, context); } if (child) { if (Array.isArray(child)) { child.forEach(item => walkTree(item, childContext, visitor)); } else { walkTree(child, childContext, visitor); } } } else if ((element.type as any)._context || (element.type as any).Consumer) { // A React context provider or consumer if (visitor(element, null, context) === false) { return; } let child; if ((element.type as any)._context) { // A provider - sets the context value before rendering children ((element.type as any)._context as any)._currentValue = element.props.value; child = element.props.children; } else { // A consumer child = element.props.children((element.type as any)._currentValue); } if (child) { if (Array.isArray(child)) { child.forEach(item => walkTree(item, context, visitor)); } else { walkTree(child, context, visitor); } } } else { // A basic string or dom element, just get children if (visitor(element, null, context) === false) { return; } if (element.props && element.props.children) { React.Children.forEach(element.props.children, (child: any) => { if (child) { walkTree(child, context, visitor); } }); } } } else if (typeof element === 'string' || typeof element === 'number') { // Just visit these, they are leaves so we don't keep traversing. visitor(element, null, context); } // TODO: Portals? } function hasFetchDataFunction(instance: React.Component<any>): instance is FetchComponent { return typeof (instance as any).fetchData === 'function'; } function isPromise<T>(promise: Object): promise is Promise<T> { return typeof (promise as any).then === 'function'; } function getPromisesFromTree({ rootElement, rootContext = {}, }: PromiseTreeArgument): PromiseTreeResult[] { const promises: PromiseTreeResult[] = []; walkTree(rootElement, rootContext, (_, instance, context, childContext) => { if (instance && hasFetchDataFunction(instance)) { const promise = instance.fetchData(); if (isPromise<Object>(promise)) { promises.push({ promise, context: childContext || context, instance }); return false; } } }); return promises; } const _getDataFromTree = ( rootElement: React.ReactNode, rootContext: any = {}, ): Promise<void> => { const promises = getPromisesFromTree({ rootElement, rootContext }); if (promises.length === 0) { return Promise.resolve(); } const errors: any[] = []; const mappedPromises = promises.map(({ promise, context, instance }) => { return promise .then(_ => _getDataFromTree(instance.render(), context)) .catch(e => errors.push(e)); }); return Promise.all(mappedPromises).then(_ => { if (errors.length > 0) { const error = errors.length === 1 ? errors[0] : new Error( `${errors.length} errors were thrown when executing your fetchData functions.`, ); error.queryErrors = errors; throw error; } }); } const getDataFromTree = (Component: React.ComponentType<any>, rootContext: any = {}) => { return (props: PropsLike): Promise<void> => { const rootElement = React.createElement(Component, props); return _getDataFromTree(rootElement, rootContext); }; }; export default getDataFromTree;