Skip to content

Instantly share code, notes, and snippets.

@hamlim
Created May 5, 2018 00:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hamlim/58aec71b7d91871ee0d001621a4969e8 to your computer and use it in GitHub Desktop.
Save hamlim/58aec71b7d91871ee0d001621a4969e8 to your computer and use it in GitHub Desktop.
Get data from tree fork with support for React.createContext
// Forked from react-apollo get-data-from-tree
import React from 'react';
const getProps = element => {
return element.props || element.attributes;
};
const isReactElement = element => {
return !!element.type;
};
const isComponentClass = Comp => {
return (
Comp.prototype && (Comp.prototype.render || Comp.prototype.isReactComponent)
);
};
const providesChildContext = instance => {
return !!instance.getChildContext;
};
const findContextValueInStack = (stack, context) => {
return stack.reduceRight((item, stackItem) => {
if (stackItem.context === context) {
return stackItem;
}
return item;
}, null);
};
// 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
// eslint-disable-next-line max-params
export const walkTree = (element, context, visitor, stack = []) => {
if (Array.isArray(element)) {
element.forEach(item => walkTree(item, context, visitor, stack));
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 = {...Comp.defaultProps, ...getProps(element)};
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 (isComponentClass(Comp)) {
const instance = new Comp(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;
// 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 well only support setState in
// componentWillMount, which happens *before* render).
instance.setState = newState => {
let updatedState = newState;
if (typeof newState === 'function') {
updatedState = newState(
instance.state,
instance.props,
instance.context
);
}
instance.state = {...instance.state, ...updatedState};
};
// 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 (providesChildContext(instance)) {
childContext = {...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;
}
// eslint-disable-next-line new-cap
child = Comp(props, context);
}
if (child) {
if (Array.isArray(child)) {
child.forEach(item => walkTree(item, childContext, visitor, stack));
} else {
walkTree(child, childContext, visitor, stack);
}
}
} else {
// a basic string or dom element, just get children
if (visitor(element, null, context) === false) {
return;
}
// Context.Provider
if (element.type && element.type._context) {
stack.push({
value: element.props.value,
context: element.type._context
});
}
// Context.Consumer
// Context Consumers are identified by the fact that they have a reference
// to their provider set on their type, this is the internal React structure of
// a Consumer component:
// {
// type: {
// Provider: {
// _context: { ... }
// },
// Consumer: ...,
// }
// }
//
if (element && element.type && element.type.Provider) {
// Search for matching context from the right in the stack (reverse chronological order)
const found = findContextValueInStack(
stack,
element.type.Provider._context
);
// If we didn't find a matching context value, just use the default
const consumerValue =
found === null ? element.type._defaultValue : found.value;
// instantiate the consumers children
const child = element.props.children(consumerValue);
if (child) {
// recurse through consumer children
walkTree(child, context, visitor, stack);
}
}
if (element.props && element.props.children) {
React.Children.forEach(element.props.children, child => {
if (child) {
walkTree(child, context, visitor, stack);
}
});
if (element.type && element.type._context) {
// We have completed a Provider stack
// lets remove the last item from the stack
stack.pop();
}
}
}
} 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?
};
const hasFetchDataFunction = instance => {
return typeof instance.fetchData === 'function';
};
const isPromise = promise => {
return typeof promise.then === 'function';
};
const getPromisesFromTree = ({rootElement, rootContext = {}}) => {
const promises = [];
// eslint-disable-next-line max-params
walkTree(rootElement, rootContext, (_, instance, context, childContext) => {
if (instance && hasFetchDataFunction(instance)) {
const promise = instance.fetchData();
if (isPromise(promise)) {
promises.push({
promise,
context: childContext || context,
instance
});
return false;
}
}
});
return promises;
};
export default function getDataFromTree(rootElement, rootContext = {}) {
const promises = getPromisesFromTree({rootElement, rootContext});
if (!promises.length) {
return Promise.resolve();
}
const errors = [];
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;
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment