Skip to content

Commit

Permalink
feat: support React.createContext API (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
gregberge committed Apr 18, 2018
1 parent fa3587b commit 289ad67
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 220 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules/
__fixtures__/
/example/
CHANGELOG.md
package.json
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-eslint": "^8.2.3",
"babel-jest": "^22.4.3",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-external-helpers": "^6.22.0",
Expand All @@ -49,24 +49,24 @@
"eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.7.0",
"jest": "^22.4.3",
"prettier": "^1.11.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"prettier": "^1.12.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-router": "^4.2.0",
"react-test-renderer": "^16.2.0",
"rollup": "^0.57.1",
"react-test-renderer": "^16.3.2",
"rollup": "^0.58.0",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-uglify": "^3.0.0",
"rollup-plugin-visualizer": "^0.4.0",
"rollup-plugin-visualizer": "^0.6.0",
"standard-version": "^4.3.0"
},
"dependencies": {
Expand Down
169 changes: 112 additions & 57 deletions src/server/index.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,134 @@
/* eslint-disable react/no-danger */
import { Children } from 'react'
/* eslint-disable react/no-danger, no-underscore-dangle */
import React from 'react'
import { LOADABLE } from '../constants'
import DeferredState from './DeferredState'

function isReactElement(element) {
return !!element.type
}

function isComponentClass(Comp) {
return (
Comp.prototype && (Comp.prototype.render || Comp.prototype.isReactComponent)
)
}

function providesChildContext(instance) {
return !!instance.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
// or recurse into its child elements
export function walkTree(element, context, visitor) {
const Component = element.type
if (Array.isArray(element)) {
element.forEach(item => walkTree(item, context, visitor))
return
}

if (!element) {
return
}

// a stateless functional component or a class
if (typeof Component === 'function') {
const props = { ...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 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 = nextState => {
instance.state = { ...instance.state, ...nextState }
}
if (isReactElement(element)) {
if (typeof element.type === 'function') {
const Comp = element.type
const props = Object.assign({}, Comp.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 (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 => {
if (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(
instance.state,
instance.props,
instance.context,
)
}
instance.state = Object.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()
}
// 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 = { ...context, ...instance.getChildContext() }
}
if (providesChildContext(instance)) {
childContext = Object.assign({}, context, instance.getChildContext())
}

if (visitor(element, instance, context) === false) {
return
if (visitor(element, instance, context, childContext) === false) {
return
}

child = instance.render()
} else {
// just a stateless functional
if (visitor(element, null, context) === false) {
return
}

child = Comp(props, context)
}

child = instance.render()
if (child) {
if (Array.isArray(child)) {
child.forEach(item => walkTree(item, childContext, visitor))
} else {
walkTree(child, childContext, visitor)
}
}
} else {
// just a stateless functional
// a basic string or dom element, just get children
if (visitor(element, null, context) === false) {
return
}

// typescript casting for stateless component
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
}
// Context.Provider
if (element.type && element.type._context) {
element.type._context._currentValue = element.props.value
}

if (element.props && element.props.children) {
Children.forEach(element.props.children, (child: any) => {
// Context.Consumer
if (element && element.type && element.type.Provider) {
const child = element.props.children(element.type._currentValue)
if (child) {
walkTree(child, context, visitor)
}
})
}

if (element.props && element.props.children) {
React.Children.forEach(element.props.children, (child: any) => {
if (child) {
walkTree(child, context, visitor)
}
})
}
}
} 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?
}

function getQueriesFromTree(
Expand Down Expand Up @@ -140,13 +195,13 @@ export function getLoadableState(
return Promise.all(mappedQueries).then(() => {
if (errors.length > 0) {
if (errors.length === 1) {
throw errors[0];
throw errors[0]
} else {
const err = new Error(
`${errors.length} errors were thrown when importing your modules.`
);
`${errors.length} errors were thrown when importing your modules.`,
)
err.queryErrors = errors
throw err;
throw err
}
}

Expand Down
28 changes: 25 additions & 3 deletions src/server/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ describe('server side rendering', () => {
</div>
)

const context = {}

app = (
<StaticRouter location="/books/2" context={context}>
<StaticRouter location="/books/2" context={{}}>
<App />
</StaticRouter>
)
Expand All @@ -30,6 +28,30 @@ describe('server side rendering', () => {
})
})

describe('React.createContext', () => {
beforeEach(() => {
const Context = React.createContext(false)
const App = () => (
<StaticRouter location="/books/2" context={{}}>
<Context.Provider value>
<Context.Consumer>
{value => value && <Route path="/books" component={Books} />}
</Context.Consumer>
</Context.Provider>
</StaticRouter>
)

app = <App />
})

it.only('should traverse context', async () => {
const loadableState = await getLoadableState(app)
expect(loadableState.tree).toEqual({
children: [{ children: [{ id: './Book' }], id: './Books' }],
})
})
})

describe('without any ids', () => {
it('should return an empty deferred state', async () => {
const context = {}
Expand Down
Loading

0 comments on commit 289ad67

Please sign in to comment.