All files / src getDataFromTree.ts

100% Statements 66/66
100% Branches 16/16
100% Functions 16/16
100% Lines 55/55
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 13427x 27x 27x         27x 80x 80x   140x 70x 70x 108x 108x       64x 32x 32x 64x 64x 64x 10x   54x     32x   27x   27x   26x       26x   27x       70x 70x 32x     32x         32x   38x     27x 48x     27x 24x 24x 32x                   32x 32x   24x 24x   27x   27x     27x       27x 49x         27x 49x   27x   27x         51x   26x     49x                       48x         26x    
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { renderToStaticMarkup } from 'react-dom/server';
import Query from './Query';
 
// Like a Set, but for tuples. In practice, this class is used to store
// (query, JSON.stringify(variables)) tuples.
class Trie {
  private children: Map<any, Trie> | null = null;
  private added = false;
 
  has(...keys: any[]) {
    let node: Trie = this;
    return keys.every(key => {
      const child = node.children && node.children.get(key);
      return !!(child && (node = child));
    }) && node.added;
  }
 
  add(...keys: any[]) {
    let node: Trie = this;
    keys.forEach(key => {
      const map = node.children || (node.children = new Map);
      const child = map.get(key);
      if (child) {
        node = child;
      } else {
        map.set(key, node = new Trie());
      }
    });
    node.added = true;
  }
}
 
export class RenderPromises {
  // Map from Query component instances to pending fetchData promises.
  private queryPromises = new Map<Query<any, any>, Promise<any>>();
 
  // A way of remembering queries we've seen during previous renderings,
  // so that we never attempt to fetch them again in future renderings.
  private queryGraveyard = new Trie();
 
  public addQueryPromise<TData, TVariables>(
    queryInstance: Query<TData, TVariables>,
    finish: () => React.ReactNode,
  ): React.ReactNode {
    const { query, variables } = queryInstance.props;
    if (!this.queryGraveyard.has(query, JSON.stringify(variables))) {
      this.queryPromises.set(
        queryInstance,
        new Promise(resolve => {
          resolve(queryInstance.fetchData());
        }),
      );
      // Render null to abandon this subtree for this rendering, so that we
      // can wait for the data to arrive.
      return null;
    }
    return finish();
  }
 
  public hasPromises() {
    return this.queryPromises.size > 0;
  }
 
  public consumeAndAwaitPromises() {
    const promises: Promise<any>[] = [];
    this.queryPromises.forEach((promise, queryInstance) => {
      const { query, variables } = queryInstance.props;
      // Make sure we never try to call fetchData for this query document and
      // these variables again. Since the queryInstance objects change with
      // every rendering, deduplicating them by query and variables is the
      // best we can do. If a different Query component happens to have the
      // same query document and variables, it will be immediately rendered
      // by calling finish() in addQueryPromise, which could result in the
      // rendering of an unwanted loading state, but that's not nearly as bad
      // as getting stuck in an infinite rendering loop because we kept calling
      // queryInstance.fetchData for the same Query component indefinitely.
      this.queryGraveyard.add(query, JSON.stringify(variables));
      promises.push(promise);
    });
    this.queryPromises.clear();
    return Promise.all(promises);
  }
}
 
class RenderPromisesProvider extends React.Component<{
  renderPromises: RenderPromises;
}> {
  static childContextTypes = {
    renderPromises: PropTypes.object,
  };
 
  getChildContext() {
    return {
      renderPromises: this.props.renderPromises,
    };
  }
 
  render() {
    return this.props.children;
  }
}
 
export default function getDataFromTree(
  rootElement: React.ReactNode,
  // The rendering function is configurable! We use renderToStaticMarkup as
  // the default, because it's a little less expensive than renderToString,
  // and legacy usage of getDataFromTree ignores the return value anyway.
  renderFunction = renderToStaticMarkup,
): Promise<string> {
  const renderPromises = new RenderPromises();
 
  function process(): Promise<string> | string {
    const html = renderFunction(
      React.createElement(RenderPromisesProvider, {
        renderPromises,
        // Always re-render from the rootElement, even though it might seem
        // better to render the children of the component responsible for the
        // promise, because it is not possible to reconstruct the full context
        // of the original rendering (including all unknown context provider
        // elements) for a subtree of the orginal component tree.
        children: rootElement,
      })
    );
 
    return renderPromises.hasPromises()
      ? renderPromises.consumeAndAwaitPromises().then(process)
      : html;
  }
 
  return Promise.resolve().then(process);
}