UNPKG

react-apollo

Version:
168 lines (140 loc) 5.72 kB
import { Children, ReactElement, ComponentClass, StatelessComponent } from 'react'; import * as ReactDOM from 'react-dom/server'; import ApolloClient, { ApolloQueryResult } from 'apollo-client'; const assign = require('object-assign'); export declare interface Context { client?: ApolloClient; store?: any; [key: string]: any; } export declare interface QueryTreeArgument { rootElement: ReactElement<any>; rootContext?: Context; } export declare interface QueryResult { query: Promise<ApolloQueryResult<any>>; element: ReactElement<any>; context: Context; } // 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: ReactElement<any>, context: Context, visitor: (element: ReactElement<any>, instance: any, context: Context) => boolean | void, ) { const Component = element.type; // a stateless functional component or a class if (typeof Component === 'function') { const props = assign({}, Component.defaultProps, element.props); let childContext = context; let child; // Are we are a react class? // https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L66 if (Component.prototype && Component.prototype.isReactComponent) { // typescript force casting since typescript doesn't have definitions for class // methods const _component = Component as any; const instance = new _component(props, context); // In case the user doesn't pass these to super in the constructor instance.props = instance.props || props; instance.context = instance.context || context; // 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 well only support setState in // componentWillMount, which happens *before* render). instance.setState = (newState) => { instance.state = assign({}, instance.state, newState); }; // this is a poor man's version of // https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L181 if (instance.componentWillMount) { instance.componentWillMount(); } if (instance.getChildContext) { childContext = assign({}, context, instance.getChildContext()); } if (visitor(element, instance, context) === false) { return; } child = instance.render(); } else { // just a stateless functional if (visitor(element, null, context) === false) { return; } // typescript casting for stateless component const _component = Component as StatelessComponent<any>; child = _component(props, context); } if (child) { walkTree(child, childContext, visitor); } } else { // a basic string or dom element, just get children if (visitor(element, null, context) === false) { return; } if (element.props && element.props.children) { Children.forEach(element.props.children, (child: any) => { if (child) { walkTree(child, context, visitor); } }); } } } function getQueriesFromTree( { rootElement, rootContext = {} }: QueryTreeArgument, fetchRoot: boolean = true, ): QueryResult[] { const queries = []; walkTree(rootElement, rootContext, (element, instance, context) => { const skipRoot = !fetchRoot && (element === rootElement); if (instance && typeof instance.fetchData === 'function' && !skipRoot) { const query = instance.fetchData(); if (query) { queries.push({ query, element, context }); // Tell walkTree to not recurse inside this component; we will // wait for the query to execute before attempting it. return false; } } }); return queries; } // XXX component Cache export function getDataFromTree(rootElement: ReactElement<any>, rootContext: any = {}, fetchRoot: boolean = true): Promise<void> { let queries = getQueriesFromTree({ rootElement, rootContext }, fetchRoot); // no queries found, nothing to do if (!queries.length) return Promise.resolve(); const errors = []; // wait on each query that we found, re-rendering the subtree when it's done const mappedQueries = queries.map(({ query, element, context }) => { // we've just grabbed the query for element, so don't try and get it again return ( query .then(_ => getDataFromTree(element, context, false)) .catch(e => errors.push(e)) ); }); // Run all queries. If there are errors, still wait for all queries to execute // so the caller can ignore them if they wish. See https://github.com/apollographql/react-apollo/pull/488#issuecomment-284415525 return Promise.all(mappedQueries).then(_ => { if (errors.length > 0) { const error = errors.length === 1 ? errors[0] : new Error(`${errors.length} errors were thrown when executing your GraphQL queries.`); error.queryErrors = errors; throw error; } }); } export function renderToStringWithData(component: ReactElement<any>): Promise<string> { return getDataFromTree(component) .then(() => ReactDOM.renderToString(component)); } export function cleanupApolloState(apolloState) { for (let queryId in apolloState.queries) { let fieldsToNotShip = ['minimizedQuery', 'minimizedQueryString']; for (let field of fieldsToNotShip) delete apolloState.queries[queryId][field]; } }