All files / src walkTree.ts

90.54% Statements 67/74
84.06% Branches 58/69
100% Functions 10/10
90.14% Lines 64/71
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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 17427x                     18x       104x       18x           10x           27x                   136x   112x 6x     106x 6x 2x       104x 91x 18x 18x 18x 18x     18x 10x       10x     10x     10x           10x 1x           1x     10x 1x 1x 1x   9x 1x 8x       10x       10x       10x     8x       8x     18x 16x 10x   11x     73x   8x       8x 8x     2x 2x 2x     6x 6x 4x   6x     8x 8x 4x   6x         65x       65x 20x 52x 51x         13x   13x        
import * as React from 'react';
 
export interface Context {
  [key: string]: any;
}
 
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 isReactElement(element: React.ReactNode): element is React.ReactElement<any> {
  return !!(element as any).type;
}
 
function isComponentClass(Comp: React.ComponentType<any>): Comp is React.ComponentClass<any> {
  return Comp.prototype && (Comp.prototype.render || Comp.prototype.isReactComponent);
}
 
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,
    newContextMap: Map<any, any>,
    context: Context,
    childContext?: Context,
  ) => boolean | void,
  newContext: Map<any, any> = new Map(),
) {
  if (!element) {
    return;
  }
 
  if (Array.isArray(element)) {
    element.forEach(item => walkTree(item, context, visitor, newContext));
    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 => {
          Iif (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);
          Eif (result !== null) {
            instance.state = Object.assign({}, instance.state, result);
          }
        } else if (instance.UNSAFE_componentWillMount) {
          instance.UNSAFE_componentWillMount();
        } else Iif (instance.componentWillMount) {
          instance.componentWillMount();
        }
 
        Iif (providesChildContext(instance)) {
          childContext = Object.assign({}, context, instance.getChildContext());
        }
 
        Iif (visitor(element, instance, newContext, context, childContext) === false) {
          return;
        }
 
        child = instance.render();
      } else {
        // Just a stateless functional
        Iif (visitor(element, null, newContext, context) === false) {
          return;
        }
 
        child = Comp(props, context);
      }
 
      if (child) {
        if (Array.isArray(child)) {
          child.forEach(item => walkTree(item, childContext, visitor, newContext));
        } else {
          walkTree(child, childContext, visitor, newContext);
        }
      }
    } else if ((element.type as any)._context || (element.type as any).Consumer) {
      // A React context provider or consumer
      Iif (visitor(element, null, newContext, context) === false) {
        return;
      }
 
      let child;
      if (!!(element.type as any)._context) {
        // A provider - sets the context value before rendering children
        // this needs to clone the map because this value should only apply to children of the provider
        newContext = new Map(newContext);
        newContext.set(element.type, element.props.value);
        child = element.props.children;
      } else {
        // A consumer
        let value = (element.type as any)._currentValue;
        if (newContext.has((element.type as any).Provider)) {
          value = newContext.get((element.type as any).Provider);
        }
        child = element.props.children(value);
      }
 
      Eif (child) {
        if (Array.isArray(child)) {
          child.forEach(item => walkTree(item, context, visitor, newContext));
        } else {
          walkTree(child, context, visitor, newContext);
        }
      }
    } else {
      // A basic string or dom element, just get children
      Iif (visitor(element, null, newContext, context) === false) {
        return;
      }
 
      if (element.props && element.props.children) {
        React.Children.forEach(element.props.children, (child: any) => {
          if (child) {
            walkTree(child, context, visitor, newContext);
          }
        });
      }
    }
  } else Eif (typeof element === 'string' || typeof element === 'number') {
    // Just visit these, they are leaves so we don't keep traversing.
    visitor(element, null, newContext, context);
  }
  // TODO: Portals?
}