From 086a58a6bd68a1bcee5a057204ee180ba957cff8 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Tue, 16 Jan 2018 22:25:17 +1100 Subject: [PATCH 1/2] Support React.Fragments. #799 --- .../react-hot-loader/src/internal/reactUtils.js | 4 ++++ .../src/reconciler/hotReplacementRender.js | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/react-hot-loader/src/internal/reactUtils.js b/packages/react-hot-loader/src/internal/reactUtils.js index 1580a060c..ed751cd67 100644 --- a/packages/react-hot-loader/src/internal/reactUtils.js +++ b/packages/react-hot-loader/src/internal/reactUtils.js @@ -1,3 +1,4 @@ +import React from 'react' /* eslint-disable no-underscore-dangle */ export const isCompositeComponent = type => typeof type === 'function' @@ -18,3 +19,6 @@ export const updateInstance = instance => { updater.enqueueForceUpdate(instance) } } + +export const isFragmentNode = ({ type }) => + React.Fragment && type === React.Fragment diff --git a/packages/react-hot-loader/src/reconciler/hotReplacementRender.js b/packages/react-hot-loader/src/reconciler/hotReplacementRender.js index 55545b286..ea5e5abf5 100644 --- a/packages/react-hot-loader/src/reconciler/hotReplacementRender.js +++ b/packages/react-hot-loader/src/reconciler/hotReplacementRender.js @@ -1,7 +1,11 @@ import { PROXY_KEY, UNWRAP_PROXY } from 'react-stand-in' import levenshtein from 'fast-levenshtein' import { getIdByType, updateProxyById } from './proxies' -import { updateInstance, getComponentDisplayName } from '../internal/reactUtils' +import { + updateInstance, + getComponentDisplayName, + isFragmentNode, +} from '../internal/reactUtils' import reactHotLoader from '../reactHotLoader' import logger from '../logger' @@ -156,8 +160,17 @@ const mergeInject = (a, b, instance) => { return NO_CHILDREN } +const transformFlowNode = flow => + flow.reduce((acc, node) => { + if (isFragmentNode(node) && node.props && node.props.children) { + return [...acc, ...node.props.children] + } else { + return [...acc, node] + } + }, []) + const hotReplacementRender = (instance, stack) => { - const flow = filterNullArray(asArray(render(instance))) + const flow = transformFlowNode(filterNullArray(asArray(render(instance)))) const { children } = stack From 7923d79036ee5389876eda3be143bab7c7a8762e Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Fri, 19 Jan 2018 19:38:35 +1100 Subject: [PATCH 2/2] add tests --- README.md | 7 +- .../src/reconciler/hotReplacementRender.js | 3 +- .../test/AppContainer.dev.test.js | 73 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 76d587747..f233bd2e2 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,11 @@ const App = () =>
Hello World!
export default hot(module)(App) ``` + `Hot` accepts only React Component (Stateful or Stateless), resulting the `HotExported` variant of it. -The `hot` function will setup current module to _self-accept_ itself on reload, and will __ignore__ all the changes, made for non-React components. -You may mark as much modules as you want. But `HotExportedComponent` __should be the only used export__ of a _hot_-module. +The `hot` function will setup current module to _self-accept_ itself on reload, and will **ignore** all the changes, made for non-React components. +You may mark as much modules as you want. But `HotExportedComponent` **should be the only used export** of a _hot_-module. + > Note: Please note how often we have used `exported` keyword. `hot` is for exports. > Note: does nothing in production mode, just passes App through. @@ -96,6 +98,7 @@ export default hot(module)(App) ``` ### Migrating from [create-react-app](https://github.com/facebookincubator/create-react-app) without ejecting + Users [reports](https://github.com/gaearon/react-hot-loader/pull/729#issuecomment-354097936), that it is possible to use [react-app-rewire-hot-loader](https://github.com/cdharris/react-app-rewire-hot-loader) to setup React-hot-loader without ejecting. Follow [these code examples](https://github.com/Grimones/cra-rhl/commit/4ed74af2dc649301695f67df05a12f210fb7820c) to repeat the approach. diff --git a/packages/react-hot-loader/src/reconciler/hotReplacementRender.js b/packages/react-hot-loader/src/reconciler/hotReplacementRender.js index ea5e5abf5..28685778e 100644 --- a/packages/react-hot-loader/src/reconciler/hotReplacementRender.js +++ b/packages/react-hot-loader/src/reconciler/hotReplacementRender.js @@ -164,9 +164,8 @@ const transformFlowNode = flow => flow.reduce((acc, node) => { if (isFragmentNode(node) && node.props && node.props.children) { return [...acc, ...node.props.children] - } else { - return [...acc, node] } + return [...acc, node] }, []) const hotReplacementRender = (instance, stack) => { diff --git a/packages/react-hot-loader/test/AppContainer.dev.test.js b/packages/react-hot-loader/test/AppContainer.dev.test.js index 5bcb34331..ba37e13f9 100644 --- a/packages/react-hot-loader/test/AppContainer.dev.test.js +++ b/packages/react-hot-loader/test/AppContainer.dev.test.js @@ -1456,6 +1456,79 @@ describe(`AppContainer (dev)`, () => { expect(wrapper.update().text()).toBe('PATCHED + 6 v2') }) + it('hot-reloads children inside Fragments', () => { + if (React.version.startsWith('16')) { + const unmount = jest.fn() + class InnerComponent extends Component { + componentWillUnmount() { + unmount() + } + + render() { + return
OldInnerComponent
+ } + } + InnerComponent.displayName = 'InnerComponent' + + const InnerItem = () => ( + + -1- + + ) + RHL.register(InnerItem, 'InnerItem', 'test.js') + + const Item = () => ( + +
  • 1
  • +
  • + +
  • +
  • 3
  • +
    + ) + // + const App = () => ( + + ) + + const wrapper = mount( + + + , + ) + + expect(wrapper.update().text()).toBe('1-1-OldInnerComponent3') + { + class InnerComponent extends Component { + componentWillUnmount() { + unmount() + } + + render() { + return
    NewInnerComponent
    + } + } + InnerComponent.displayName = 'InnerComponent' + + const InnerItem = () => ( + + -2- + + ) + RHL.register(InnerItem, 'InnerItem', 'test.js') + + wrapper.setProps({ children: }) + } + expect(unmount).toHaveBeenCalledTimes(0) + expect(wrapper.update().text()).toBe('1-2-NewInnerComponent3') + } else { + // React 15 is always ok + expect(true).toBe(true) + } + }) + it('hot-reloads children without losing state', () => { class App extends Component { constructor(props) {