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

Making RHL more React 16.3 compatible #918 #927

Merged
merged 5 commits into from
Apr 18, 2018
Merged
Show file tree
Hide file tree
Changes from all 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: 1 addition & 1 deletion examples/styled-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-emotion": "^8.0.12",
"react-hot-loader": "next",
"react-hot-loader": "^4.0.1",
"styled-components": "^2.4.0"
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"global": "^4.3.0",
"hoist-non-react-statics": "^2.5.0",
"prop-types": "^15.6.1",
"react-lifecycles-compat": "^2.0.0",
"shallowequal": "^1.0.2"
},
"engines": {
Expand Down
35 changes: 14 additions & 21 deletions src/AppContainer.dev.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'
import { polyfill } from 'react-lifecycles-compat'
import logger from './logger'
import { get as getGeneration } from './global/generation'
import { renderReconciler } from './reconciler/proxyAdapter'
import { flushScheduledUpdates } from './reconciler'

class AppContainer extends React.Component {
constructor(props) {
super(props)

this.state = {
error: null,
generation: 0,
}
}

componentWillReceiveProps() {
if (this.state.generation !== getGeneration()) {
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.generation !== getGeneration()) {
// Hot reload is happening.

this.setState({
return {
error: null,
generation: getGeneration(),
})

// perform sandboxed render to find similarities between new and old code
renderReconciler(this, true)
// it is possible to flush update out of render cycle
flushScheduledUpdates()
}
}
return null
}

state = {
error: null,
// eslint-disable-next-line react/no-unused-state
generation: 0,
}

shouldComponentUpdate(prevProps, prevState) {
Expand Down Expand Up @@ -72,4 +63,6 @@ AppContainer.propTypes = {
errorReporter: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
}

polyfill(AppContainer)

export default AppContainer
2 changes: 1 addition & 1 deletion src/hot.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const hot = sourceModule => {
return createHoc(
WrappedComponent,
class ExportedComponent extends Component {
componentWillMount() {
componentDidMount() {
module.instances.push(this)
}

Expand Down
2 changes: 1 addition & 1 deletion src/internal/stack/hydrateFiberStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function pushStack(stack, node) {

if (!stack.instance) {
stack.instance = {
SFC_fake: true,
SFC_fake: stack.type,
props: {},
render: () => stack.type(stack.instance.props),
}
Expand Down
2 changes: 1 addition & 1 deletion src/internal/stack/hydrateLegacyStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function pushState(stack, type, instance) {
if (typeof type === 'function' && type.isStatelessFunctionalProxy) {
// In React 15 SFC is wrapped by component. We have to detect our proxies and change the way it works
stack.instance = {
SFC_fake: true,
SFC_fake: type,
props: {},
render: () => type(stack.instance.props),
}
Expand Down
28 changes: 20 additions & 8 deletions src/proxy/createClassProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const resetClassProxies = () => {
const blackListedClassMembers = [
'constructor',
'render',
'componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'componentWillUnmount',
Expand All @@ -38,8 +39,8 @@ const blackListedClassMembers = [
]

const defaultRenderOptions = {
componentWillReceiveProps: identity,
componentWillRender: identity,
componentDidUpdate: result => result,
componentDidRender: result => result,
}

Expand All @@ -56,6 +57,14 @@ const defineClassMembers = (Class, methods) =>
defineClassMember(Class, methodName, methods[methodName]),
)

const setSFPFlag = (component, flag) =>
safeDefineProperty(component, 'isStatelessFunctionalProxy', {
configurable: false,
writable: false,
enumerable: false,
value: flag,
})

function createClassProxy(InitialComponent, proxyKey, options) {
const renderOptions = {
...defaultRenderOptions,
Expand Down Expand Up @@ -129,9 +138,9 @@ function createClassProxy(InitialComponent, proxyKey, options) {
target[PROXY_IS_MOUNTED] = true
},
)
const componentWillReceiveProps = lifeCycleWrapperFactory(
'componentWillReceiveProps',
renderOptions.componentWillReceiveProps,
const componentDidUpdate = lifeCycleWrapperFactory(
'componentDidUpdate',
renderOptions.componentDidUpdate,
)
const componentWillUnmount = lifeCycleWrapperFactory(
'componentWillUnmount',
Expand All @@ -157,7 +166,7 @@ function createClassProxy(InitialComponent, proxyKey, options) {
result = CurrentComponent.prototype.render.call(this)
}

return renderOptions.componentDidRender(result)
return renderOptions.componentDidRender.call(this, result)
}

function proxiedRender() {
Expand All @@ -171,7 +180,7 @@ function createClassProxy(InitialComponent, proxyKey, options) {
render: proxiedRender,
hotComponentRender,
componentDidMount,
componentWillReceiveProps,
componentDidUpdate,
componentWillUnmount,
})
}
Expand All @@ -196,10 +205,13 @@ function createClassProxy(InitialComponent, proxyKey, options) {

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

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

// This is a Relay-style container constructor. We can't do the prototype-
// style wrapping for this as we do elsewhere, so just we just pass it
Expand Down
1 change: 1 addition & 0 deletions src/proxy/transferStaticProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const RESERVED_STATICS = [
'prototype',
'toString',
'valueOf',
'isStatelessFunctionalProxy',
PROXY_KEY,
UNWRAP_PROXY,
]
Expand Down
37 changes: 34 additions & 3 deletions src/reconciler/hotReplacementRender.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ const UNDEFINED_NAMES = {
Component: true,
}

let renderStack = []

const stackReport = () => {
const rev = renderStack.slice().reverse()
logger.warn('in', rev[0].name, rev)
}

const areNamesEqual = (a, b) =>
a === b || (UNDEFINED_NAMES[a] && UNDEFINED_NAMES[b])
const isReactClass = fn => fn && !!fn.render
Expand Down Expand Up @@ -105,7 +112,7 @@ const render = component => {
}
if (isReactClass(component)) {
// not calling real render method to prevent call recursion.
// stateless componets does not have hotComponentRender
// stateless components does not have hotComponentRender
return component.hotComponentRender
? component.hotComponentRender()
: component.render()
Expand All @@ -123,7 +130,7 @@ const render = component => {
const NO_CHILDREN = { children: [] }
const mapChildren = (children, instances) => ({
children: children.filter(c => c).map((child, index) => {
if (typeof child !== 'object') {
if (typeof child !== 'object' || child.isMerged) {
return child
}
const instanceLine = instances[index] || {}
Expand All @@ -140,10 +147,13 @@ const mapChildren = (children, instances) => ({
(child.props && child.props.children) || child.children || [],
)
const nextChildren =
oldChildren.length && mapChildren(newChildren, oldChildren)
child.type !== 'function' &&
oldChildren.length &&
mapChildren(newChildren, oldChildren)

return {
nextProps: child.props,
isMerged: true,
...instanceLine,
// actually child merge is needed only for "HTML TAG"s, and usually don't work for Components.
// the children from an instance or rendered children
Expand Down Expand Up @@ -197,6 +207,7 @@ const mergeInject = (a, b, instance) => {
'and children of ',
instance,
)
stackReport()
}
return NO_CHILDREN
}
Expand All @@ -221,6 +232,10 @@ export const flushScheduledUpdates = () => {
)
}

export const unscheduleUpdate = instance => {
scheduledUpdates = scheduledUpdates.filter(inst => inst !== instance)
}

const scheduleInstanceUpdate = instance => {
scheduledUpdates.push(instance)
if (!scheduledUpdate) {
Expand All @@ -229,6 +244,14 @@ const scheduleInstanceUpdate = instance => {
}

const hotReplacementRender = (instance, stack) => {
if (isReactClass(instance)) {
const type = getElementType(stack)
renderStack.push({
name: getComponentDisplayName(type),
type,
props: stack.instance.props,
})
}
const flow = transformFlowNode(filterNullArray(asArray(render(instance))))

const { children } = stack
Expand Down Expand Up @@ -266,6 +289,7 @@ const hotReplacementRender = (instance, stack) => {
'instead of',
stackChild.type,
)
stackReport()
}
return
}
Expand All @@ -290,6 +314,7 @@ const hotReplacementRender = (instance, stack) => {
' - no instrumentation found. ',
'Please require react-hot-loader before React. More in troubleshooting.',
)
stackReport()
throw new Error('React-hot-loader: wrong configuration')
}

Expand All @@ -311,17 +336,23 @@ const hotReplacementRender = (instance, stack) => {
)} was expected.
${childType}`,
)
stackReport()
}

scheduleInstanceUpdate(stackChild.instance)
}
})

if (isReactClass(instance)) {
renderStack.pop()
}
}

export default (instance, stack) => {
try {
// disable reconciler to prevent upcoming components from proxying.
reactHotLoader.disableProxyCreation = true
renderStack = []
hotReplacementRender(instance, stack)
} catch (e) {
logger.warn('React-hot-loader: reconcilation failed due to error', e)
Expand Down
5 changes: 3 additions & 2 deletions src/reconciler/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import getReactStack from '../internal/getReactStack'
import hotReplacementRender, {
flushScheduledUpdates
flushScheduledUpdates,
unscheduleUpdate
} from './hotReplacementRender'

const reconcileHotReplacement = ReactInstance =>
hotReplacementRender(ReactInstance, getReactStack(ReactInstance))

export { flushScheduledUpdates }
export { flushScheduledUpdates, unscheduleUpdate }

export default reconcileHotReplacement
17 changes: 9 additions & 8 deletions src/reconciler/proxyAdapter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import reactHotLoader from '../reactHotLoader'
import { get as getGeneration } from '../global/generation'
import { getProxyByType, setStandInOptions } from './proxies'
import reconcileHotReplacement, { flushScheduledUpdates } from './index'
import reconcileHotReplacement, {
flushScheduledUpdates,
unscheduleUpdate,
} from './index'

export const RENDERED_GENERATION = 'REACT_HOT_LOADER_RENDERED_GENERATION'

Expand All @@ -28,14 +31,12 @@ function asyncReconciledRender(target) {
renderReconciler(target, false)
}

function syncReconciledRender(target) {
if (renderReconciler(target, false)) {
flushScheduledUpdates()
export function proxyWrapper(element) {
// post wrap on post render
if (!reactHotLoader.disableProxyCreation) {
unscheduleUpdate(this)
}
}

export const proxyWrapper = element => {
// post wrap on post render
if (!element) {
return element
}
Expand All @@ -55,7 +56,7 @@ export const proxyWrapper = element => {
}

setStandInOptions({
componentWillReceiveProps: syncReconciledRender,
componentWillRender: asyncReconciledRender,
componentDidRender: proxyWrapper,
componentDidUpdate: flushScheduledUpdates,
})
Loading