Created
May 5, 2018 00:30
-
-
Save hamlim/58aec71b7d91871ee0d001621a4969e8 to your computer and use it in GitHub Desktop.
Get data from tree fork with support for React.createContext
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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