Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update related issues #977

Merged
merged 6 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ Set a new configuration for React Hot Loader.
Available options are:

* `logLevel`: specify log level, default to `"error"`, available values are: `['debug', 'log', 'warn', 'error']`
* `pureSFC`: enable Stateless Functional Component. If disabled they will be converted to React Components.
Default value: false.

```js
import { setConfig } from 'react-hot-loader'
Expand Down
1 change: 1 addition & 0 deletions src/configuration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const configuration = {
logLevel: 'error',
pureSFC: false,
}

export default configuration
13 changes: 8 additions & 5 deletions src/proxy/createClassProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
proxyClassCreator,
} from './utils'
import { inject, checkLifeCycleMethods, mergeComponents } from './inject'
import config from '../configuration'

const has = Object.prototype.hasOwnProperty

Expand Down Expand Up @@ -239,12 +240,14 @@ function createClassProxy(InitialComponent, proxyKey, options) {
const result = CurrentComponent(props, context)

// simple SFC
if (!CurrentComponent.contextTypes) {
if (!ProxyFacade.isStatelessFunctionalProxy) {
setSFPFlag(ProxyFacade, true)
}
if (config.pureSFC) {
if (!CurrentComponent.contextTypes) {
if (!ProxyFacade.isStatelessFunctionalProxy) {
setSFPFlag(ProxyFacade, true)
}

return renderOptions.componentDidRender(result)
return renderOptions.componentDidRender(result)
}
}
setSFPFlag(ProxyFacade, false)

Expand Down
33 changes: 29 additions & 4 deletions src/reconciler/hotReplacementRender.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const stackReport = () => {
logger.warn('in', rev[0].name, rev)
}

const REACT_CONTEXT_CURRENT_VALUE = '_currentValue'

const areNamesEqual = (a, b) =>
a === b || (UNDEFINED_NAMES[a] && UNDEFINED_NAMES[b])
const isReactClass = fn => fn && !!fn.render
Expand Down Expand Up @@ -214,8 +216,13 @@ const mergeInject = (a, b, instance) => {

const transformFlowNode = flow =>
flow.reduce((acc, node) => {
if (isFragmentNode(node) && node.props && node.props.children) {
return [...acc, ...filterNullArray(asArray(node.props.children))]
if (node && isFragmentNode(node)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a case Fragment inside a div.

if (node.props && node.props.children) {
return [...acc, ...filterNullArray(asArray(node.props.children))]
}
if (node.children) {
return [...acc, ...filterNullArray(asArray(node.children))]
}
}
return [...acc, node]
}, [])
Expand Down Expand Up @@ -279,6 +286,15 @@ const hotReplacementRender = (instance, stack) => {

// text node
if (typeof child !== 'object' || !stackChild || !stackChild.instance) {
if (stackChild && stackChild.children && stackChild.children.length) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a important moment, we dont log.

logger.error(
'React-hot-loader: reconciliation failed',
'could not dive into [',
child,
'] while some elements are still present in the tree.',
)
stackReport()
}
return
}

Expand All @@ -296,11 +312,20 @@ const hotReplacementRender = (instance, stack) => {
return
}

if (typeof child.type !== 'function') {
// React context consumer
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure about this. The value, stored in the type is not "right".
Might be it is better to dive into children on hydrated instance.

if (child.type && typeof child.type === 'object' && child.type.Consumer) {
next({
children: (child.props ? child.props.children : child.children[0])(
child.type[REACT_CONTEXT_CURRENT_VALUE],
),
})
} else if (typeof child.type !== 'function') {
next(
// move types from render to the instances of hydrated tree
mergeInject(
asArray(child.props ? child.props.children : child.children),
transformFlowNode(
asArray(child.props ? child.props.children : child.children),
),
stackChild.instance.children,
stackChild.instance,
),
Expand Down
25 changes: 19 additions & 6 deletions test/proxy/consistency.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react'
import { createMounter, ensureNoWarnings } from './helper'
import createProxy from '../../src/proxy'
import configuration from '../../src/configuration'

const createFixtures = () => ({
modern: {
Expand Down Expand Up @@ -349,13 +350,25 @@ describe('consistency', () => {
expect(instance instanceof App).toBe(true)
})

it('should wrap SFC by SFC', () => {
const App = () => <div />
describe('should wrap SFC by SFC', () => {
it('should wrap SFC by SFC Component', () => {
const App = () => <div />

const Proxy = createProxy(App).get()
expect('isStatelessFunctionalProxy' in Proxy).toBe(false)
mount(<Proxy />).instance()
expect(Proxy.isStatelessFunctionalProxy).toBe(true)
const Proxy = createProxy(App).get()
expect('isStatelessFunctionalProxy' in Proxy).toBe(false)
mount(<Proxy />).instance()
expect(Proxy.isStatelessFunctionalProxy).toBe(false)
})

it('should wrap SFC by SFC Pure', () => {
const App = () => <div />
configuration.pureSFC = true
const Proxy = createProxy(App).get()
expect('isStatelessFunctionalProxy' in Proxy).toBe(false)
mount(<Proxy />).instance()
configuration.pureSFC = false
expect(Proxy.isStatelessFunctionalProxy).toBe(true)
})
})

it('should wrap SFC with Context by Proxy', () => {
Expand Down
138 changes: 82 additions & 56 deletions test/reconciler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { increment as incrementGeneration } from '../src/global/generation'
import { areComponentsEqual } from '../src/utils.dev'
import logger from '../src/logger'
import reactHotLoader from '../src/reactHotLoader'
import configuration from '../src/configuration'

jest.mock('../src/logger')

Expand Down Expand Up @@ -361,72 +362,97 @@ describe('reconciler', () => {
expect(wrapper.text()).toContain(43)
})

it('should assmeble props for nested children', () => {
const RenderChildren = ({ children }) => <div>{children}</div>
const RenderProp = jest
.fn()
.mockImplementation(({ prop }) => <div>{prop}</div>)
const DefaultProp = jest.fn().mockImplementation(({ prop }) => (
<div>
{prop ? (
<div>42</div>
) : (
<div>
<div>24</div>
</div>
)}
</div>
))
DefaultProp.defaultProps = {
prop: 'defaultValue',
}

const App = () => (
<RenderChildren>
describe('should assmeble props for nested children', () => {
const testSuite = () => {
const RenderChildren = ({ children }) => <div>{children}</div>
const RenderProp = jest
.fn()
.mockImplementation(({ prop }) => <div>{prop}</div>)
const DefaultProp = jest.fn().mockImplementation(({ prop }) => (
<div>
<RenderChildren>
<div className="1">
<div className="1.1">
<div className="1.2">
<RenderProp value={42} />
{prop ? (
<div>42</div>
) : (
<div>
<div>24</div>
</div>
)}
</div>
))
DefaultProp.defaultProps = {
prop: 'defaultValue',
}

const App = () => (
<RenderChildren>
<div>
<RenderChildren>
<div className="1">
<div className="1.1">
<div className="1.2">
<RenderProp value={42} />
</div>
</div>
</div>
</div>
<div className="2">
<div className="2.1">
<RenderProp value={24} />
<DefaultProp />
<div className="2">
<div className="2.1">
<RenderProp value={24} />
<DefaultProp />
</div>
</div>
</div>
</RenderChildren>
</div>
</RenderChildren>
)
</RenderChildren>
</div>
</RenderChildren>
)

logger.warn.mockClear()
logger.warn.mockClear()

const wrapper = mount(
<AppContainer>
<div>
<App />
</div>
</AppContainer>,
)
const wrapper = mount(
<AppContainer>
<div>
<App />
</div>
</AppContainer>,
)

incrementGeneration()
wrapper.setProps({ update: 'now' })
incrementGeneration()
wrapper.setProps({ update: 'now' })
return { RenderProp, DefaultProp }
}

it('for Component SFC', () => {
const { RenderProp, DefaultProp } = testSuite()

expect(RenderProp).toHaveBeenCalledTimes(4)
expect(RenderProp.mock.calls[0][0]).toEqual({ value: 42 })
expect(RenderProp.mock.calls[1][0]).toEqual({ value: 24 })
expect(RenderProp.mock.calls[2][0]).toEqual({ value: 42 })
expect(RenderProp.mock.calls[3][0]).toEqual({ value: 24 })
expect(RenderProp).toHaveBeenCalledTimes(6)
expect(RenderProp.mock.calls[0][0]).toEqual({ value: 42 })
expect(RenderProp.mock.calls[1][0]).toEqual({ value: 24 })
expect(RenderProp.mock.calls[2][0]).toEqual({ value: 42 })
expect(RenderProp.mock.calls[3][0]).toEqual({ value: 24 })

expect(DefaultProp).toHaveBeenCalledTimes(2)
expect(DefaultProp.mock.calls[0][0]).toEqual({ prop: 'defaultValue' })
expect(DefaultProp.mock.calls[1][0]).toEqual({ prop: 'defaultValue' })
expect(DefaultProp).toHaveBeenCalledTimes(3)
expect(DefaultProp.mock.calls[0][0]).toEqual({ prop: 'defaultValue' })
expect(DefaultProp.mock.calls[1][0]).toEqual({ prop: 'defaultValue' })

expect(logger.warn).not.toHaveBeenCalled()
})

expect(logger.warn).not.toHaveBeenCalled()
it('for Pure SFC', () => {
configuration.pureSFC = true
const { RenderProp, DefaultProp } = testSuite()
configuration.pureSFC = false

expect(RenderProp).toHaveBeenCalledTimes(4)
expect(RenderProp.mock.calls[0][0]).toEqual({ value: 42 })
expect(RenderProp.mock.calls[1][0]).toEqual({ value: 24 })
expect(RenderProp.mock.calls[2][0]).toEqual({ value: 42 })
expect(RenderProp.mock.calls[3][0]).toEqual({ value: 24 })

expect(DefaultProp).toHaveBeenCalledTimes(2)
expect(DefaultProp.mock.calls[0][0]).toEqual({ prop: 'defaultValue' })
expect(DefaultProp.mock.calls[1][0]).toEqual({ prop: 'defaultValue' })

expect(logger.warn).not.toHaveBeenCalled()
})
})

describe('when an error occurs in render', () => {
Expand Down