gaearon / react-hot-boilerplate

Minimal live-editing example for React
MIT License
3.91k stars 879 forks source link

React Hot Loader 3.0 beta demo #61

Closed gaearon closed 5 years ago

gaearon commented 8 years ago

A Big Update Is Coming

React Hot Loader 3 is on the horizon, and you can try it today. It fixes some long-standing issues with both React Hot Loader and React Transform, and is intended as a replacement for both.

Some nice things about it:

The docs are not there yet, but they will be added before the final release. For now, this commit is a good reference to upgrading your project from React Hot Loader 1 to React Hot Loader 3 alpha. Then see another commit as a reference for upgrading from React Hot Loader 3 alpha to React Hot Loader 3 beta.


With lessons learned both from RHL and RT, here is a demo of a unified approach.

This is really undocumented for now, and we might change API later, so feel free to play with it at your own risk. 😉

react-hot-loader/webpack is intended to be optional. We will provide a complementary react-hot-loader/babel that detects unexported components as well. You will be able to use either, depending on whether you already use Babel or not.


Known Issues

chrisblossom commented 8 years ago

Is it possible to get this working with react-router's async routes and webpack code splitting using match on the client as described in: react-router/ServerRendering.md

I have tried without success. Any help is appreciated.

gaearon commented 8 years ago

@chrisblossom No, this hack only works for sync routes. The only solution to hot reloading with async routes is fixing https://github.com/reactjs/react-router/issues/2182.

elsassph commented 8 years ago

Great improvement, I'm finally able to use react hot-reload from Haxe, another compile-to-JS language.

Only limitation that I found: Haxe JS generation doesn't go through a loader, so I'm only using react-hot-loader/webpack to resolve the requires. It all works perfectly excepted source mapping: our JS has a source map but it's lost in the process.

gaearon commented 8 years ago

@elsassph Do you mind filing an issue in React Hot Loader? Preferably with a minimal reproducing project.

chrisblossom commented 8 years ago

@chrisblossom No, this hack only works for sync routes. The only solution to hot reloading with async routes is fixing reactjs/react-router#2182.

@gaearon Thanks for the info. I ended up disabling code splitting in development to keep HMR working.

jasonslyvia commented 8 years ago

This thread is irrelevant with RHL tech details, but just some noisy whining.

Really appreciate your effort to make our development flow much more awesome, but I'll be even more grateful if you can point out that RHL@3.0.0-alpha doesn't work with React Router currently in your OP (even there are lots of pending hacks to be reviewed and merged).

Initially I'm thrilled to know functional components hot reloading is finally supported, and just can't wait to upgrade and refactor a redux project which finished three or four months ago (in which react-router-redux was redux-simple-router).

After hours of effort to upgrade dependency and set up environment, I failed to enable hot reloading in my project, totally. I mean before upgrading, at least the hot reloading is working but just some error thrown when related with functional components. Now, it's totally not working.

I know it's my fault that I haven't read through all these conversations before upgrade and refactor, but in your OP there're too many flowers and fireworks and plus ones, it's really hard to stay cool and be suspicious.

Anyway, hot reloading is still awesome, but I'll definitely wait for the final release this time.

rybon commented 8 years ago

@jasonslyvia I too got stuck initially with getting this to work with Router Router. Turns out the magic trick is to force the Router to unmount and mount after a hot reload. See https://github.com/rybon/counter-hmr/blob/master/src/components/Root.jsx (the hmrKey bit). The actual HMR implementation is just as Dan described in this TodoMVC commit, see https://github.com/rybon/counter-hmr/blob/master/src/index.jsx. This repo is a small example, but uses all the major libraries for demonstration purposes. Check out the files in the root folder (package.json, .babelrc, webpack.config.js, server.js). I removed as many dependencies as I could from package.json to demonstrate that one should only need a few devDependencies to get it to work.

Another thing worth noting is that the routes module should only export the routes, like so https://github.com/rybon/counter-hmr/blob/master/src/routes.jsx#L5. If you export other stuff from there as well I found the HMR fails, probably because it can't resolve the dependency graph properly anymore. I solved that issue in my other apps by moving these additional exports (such as constants for route paths) to a separate file and importing from there instead.

jasonslyvia commented 8 years ago

@rybon Thank you for your solution but adding key to Router causes all sub-components' local state removed, so I guess that's not ideal.

rybon commented 8 years ago

@jasonslyvia that is why you should not be using local state :). I know it is convenient and tempting, but I found a lot of issues simply go away if you store everything in the Redux store, even state one would consider 'local' for a certain component (such as a toggle boolean to show/hide something), and keep the components stateless. Same goes for triggering side effects (setTimeout, XHR etc.) in component lifecycle methods. Again, very convenient and tempting, but keeping those things out of the dispatch -> rerender loop helps with achieving the goal of making the UI a pure and idempotent function of application state ((state) => UI). This is why functional stateless components are great! They enforce these constraints.

jasonslyvia commented 8 years ago

@rybon I guess that's off the topic and if I don't have any local state, I don't even have to use react-hot-loader at all, just webpack's module.hot API is sufficient.

gaearon commented 8 years ago

I guess that's off the topic and if I don't have any local state, I don't even have to use react-hot-loader at all, just webpack's module.hot API is sufficient.

This is correct. One of the points of RHL 3 is the process is “progressively enhancing”. You set up HMR first, then if you want to preserve the state, you do a few extra steps, can always go back easily if RHL causes problems.

gaearon commented 8 years ago

@rybon

This is not correct. If you put a unique key you might as well not use React Hot Loader at all because vanilla HMR works exactly the same way.

@jasonslyvia

I'm sorry this did not work for you yet. As you said, there are more pending fixes that may fix the problem for you later. Those are still workarounds because the underlying problem is with React Router itself. It doesn't behave the way any other components do: it doesn't accept new props. We hope to fix it on the router side but please understand this takes some effort, and React Hot Loader is a hobby project. If you want an issue to be fixed, it might be a good idea to take some time to look into it yourself and help diagnose it, or at the very least create a minimal project reproducing it so that other people can look at it later.

gaearon commented 8 years ago

Really appreciate your effort to make our development flow much more awesome, but I'll be even more grateful if you can point out that RHL@3.0.0-alpha doesn't work with React Router currently in your OP

Also I'm afraid this is wrong. It does work. Currently the limitation is that only JSX config with sync routes works. But it's incorrect to say it doesn't work at all.

I will amend the OP post to include this clarification.

in your OP there're too many flowers and fireworks and plus ones, it's really hard to stay cool and be suspicious.

I'm sorry that people are enthusiastic about this alpha version which is how it is tagged and called. The reason it is released as an alpha is so that the community can help find issues like this.

After hours of effort to upgrade dependency and set up environment, I failed to enable hot reloading in my project, totally.

In the future I suggest that if you can't afford to spend these hours, don't use alpha versions of experimental packages. ;-)

gaearon commented 8 years ago

I added “Known Issues” to the opening post.

rybon commented 8 years ago

@gaearon for my understanding, how does RHL in combination with React Router work then? In my code, I could only get it to work by using Math.random as the <Router /> key. But you are saying that should not be necessary? If so, great!

ntharim commented 8 years ago

For "Known Issues" it's probably worth it to include this as well: webpack/webpack#2399, as export default function is a common pattern for functional components which React Hot Loader 3 supports.

jasonslyvia commented 8 years ago

Saddly I'm using POJO React Router config because it's a rather huge and complex project with many routes. Anyway I guess it's my fault to blindly upgrade and refactor despite the fact of alpha version.

And I have to say that @gaearon you're so productive and responsive, that's why I love the open source community.

Many thanks!

gaearon commented 8 years ago

@jasonslyvia

No problem. I just released 3.0.0-alpha.13 which should work with POJO routes.

@rybon

In your App.js file (whatever your root component is called), export a root component like this:

export default function Root() {
  return (
    <Router>
      ...
    </Router>
  )
}

Then use it normally in <AppContainer>, i.e. <AppContainter component={Root} />. As of 3.0.0-alpha.13, this should work with any way of configuring React Router except async routes.

rybon commented 8 years ago

@gaearon you're right, it does work without the unique key hack. I've updated my example counter-hmr app accordingly. Thanks for the help!

jasonslyvia commented 8 years ago

Maybe I'm missing something but 3.0.0-alpha.13 still not working for me.

// app.js
import React from 'react';
import ReactDOM from 'react-dom';
import { syncHistoryWithStore } from 'react-router-redux';
import configureStore from './redux/configureStore';
import { AppContainer } from 'react-hot-loader';
import { browserHistory } from 'react-router';

const store = configureStore();
syncHistoryWithStore(browserHistory, store);

const Root = require('./containers/Root').default;
ReactDOM.render(
  <AppContainer component={Root} props={{
    store,
  }} />
, document.getElementById('app'));

if (module.hot) {
  module.hot.accept('./containers/Root', () => {
    ReactDOM.render(
      <AppContainer component={require('./containers/Root').default}
        props={{
          store,
        }}
      />
    , document.getElementById('app'));
  });
}
// Root.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import DevTools from './DevTools';
import routes from '../routes';
import { browserHistory } from 'react-router';

const Root = ({ store, history }) => {
  return (
    <Provider store={store}>
      {
        __DEV__ ?
        (<div>
          <Router history={browserHistory} routes={routes} />
          <DevTools />
        </div>) :
        <Router history={browserHistory} routes={routes} />
      }
    </Provider>
  );
};

export default Root;

And whenever I edited some component, I get the following output.

[HMR] Waiting for update signal from WDS...
[WDS] Hot Module Replacement enabled.
[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[react-router] You cannot change <Router routes>; it will be ignored
[HMR] Updated modules:
[HMR]  - 1363
[HMR]  - 1362
[HMR]  - 1361
[HMR]  - 1538
[HMR]  - 1360
[HMR] App is up to date.

Predictably the component is not hot reloaded. Here are some dependencies I'm using:

    "react": "~0.14.3",
    "react-dom": "~0.14.3",
    "react-redux": "~4.4.5",
    "react-router": "~2.3.0",
    "react-router-redux": "~4.0.4",
    "redux": "^3.5.2",
    "react-hot-loader": "3.0.0-alpha.13",
    "webpack": "^1.13.0",
    "webpack-dev-server": "^1.14.1",
    "babel": "^6.5.2",
    "babel-core": "^6.7.7",
    "babel-loader": "^6.2.4",

Can anyone point out where did I go wrong?

rybon commented 8 years ago

@jasonslyvia I'd say your code looks mostly OK, although you are using browserHistory instead of the passed in history in the Root component. syncHistoryWithStore(browserHistory, store); should be const history = syncHistoryWithStore(browserHistory, store);. Then, pass history in as a prop in your AppContainer.

Could you verify my app https://github.com/rybon/counter-hmr works on your end? If so, I'd check what the differences between your code and mine are and see if you can resolve them. Perhaps you can wrap routes in a function and render like so:

<Router history={history} />
    {getRoutes()}
</Router>

If nothing helps, npm cache clean rm -rf node_modules npm install? Update to the latest dependencies in your package.json?

gaearon commented 8 years ago

In any case providing a complete project that reproduces the issue would be most helpful.

gaearon commented 8 years ago

React Hot Loader 3 beta is out. Release notes: https://github.com/gaearon/react-hot-loader/releases/tag/v3.0.0-beta.0.

gaearon commented 8 years ago

@bkgunby Was your problem solved? I see your message in my email but I don't see it here :disappointed:

gaearon commented 8 years ago

Never mind. There is a serious issue when used together with React Redux: https://github.com/gaearon/react-hot-loader/issues/266. The good news is, it’s fairly easy to solve, and solving it might also solve full RR support.

alexisvincent commented 8 years ago

@gaearon Is this support to be working now with react router POJO static routes?

gaearon commented 8 years ago

POJO routes are already working in the current beta. The only thing that doesn't work today is async routes.

This is what will get fixed together with https://github.com/gaearon/react-hot-loader/issues/266 in the next beta (not solved yet).

alexisvincent commented 8 years ago

@gaearon hmm. Not working for me (SystemJS), Ill try play a bit more and maybe get a repo up to replicate. Does it need the react router hack that was posted earlier? Or should it work without monkey patching React Router

gaearon commented 8 years ago

It should work fine as is, but it is necessary that the root component gets rerendered. Just replacing the module won't be enough.

gaearon commented 8 years ago

If you can post an example I can take a look.

gaearon commented 8 years ago

v3.0.0-beta.1 fixes a few major issues. React Router support should now also be complete. Please update.

https://github.com/gaearon/react-hot-loader/releases/tag/v3.0.0-beta.1

bananatranada commented 8 years ago

@gaearon it was the same issue gaearon/react-hot-loader#266 had. But I came across your wonderful medium post on hot loading, and I realized I don't even need it for the most part lol.

Thanks for reaching out!

gaearon commented 8 years ago

@bkgunby Got it, cool! I’d appreciate if you gave 3.0.0-beta.1 a try though in case you discover more problems we can fix before the stable release 😄 . Thanks!

techniq commented 8 years ago

I'm just getting into Redux, but hot replacing reducers doesn't appear to work for me (i'm on 3.0.0-beta.1).

I've tried modifying both ../reducers/index.js, and ../reducers/counter.js and neither show the console.log and state:

[HMR] The following modules couldn't be hot updated: (They would need a full reload!)

Here is my store/configureStore.js file

import { createStore } from 'redux'
import rootReducer from '../reducers'

export default function configureStore(initialState) {
  const store = createStore(rootReducer, initialState, 
    window.devToolsExtension ? window.devToolsExtension() : undefined)

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      console.log('replacing root reducer');
      const nextReducer = require('../reducers').default
      store.replaceReducer(nextReducer)
    })
  }

  return store
}

Maybe I'm missing something?

gaearon commented 8 years ago

I'm just getting into Redux, but hot replacing reducers doesn't appear to work for me (i'm on 3.0.0-beta.1).

This shouldn’t be related to React Hot Loader, it has no opinion on what you do with reducers. It only handles React components. That said, if you remove RHL, and the issue goes away, please file a bug in RHL. It’s hard to say more without a complete project reproducing this issue.

techniq commented 8 years ago

@gaearon understood, thanks :)

leshow commented 8 years ago

Is there any way to get rid of the

[react-router] You cannot change <Router routes>; it will be ignored

error?

gaearon commented 8 years ago

@leshow Yes, you can contribute to React Router to help fix this: https://github.com/reactjs/react-router/issues/2182

framerate commented 8 years ago

Two questions after playing with this today:

1) Does anyone have a complicated example yet using redux/redux-react-router? Something like this non-working example that works would be great:

    <AppContainer>
        <Provider store={store}>
            <Router history={history}>
                <Route path="/" component={Root}>
                    <Route path="debug" component={Root} />
                </Route>
            </Router>
        </Provider>
    </AppContainer>, document.getElementById('container'));

if (module.hot) {
    module.hot.accept('./components/root.jsx', () => {
        ReactDOM.render(
            <AppContainer
                component={Root} props={{store}}
            />, document.getElementById('container'));
    });
}

2) The examples I see have:

if (module.hot) {
   //...
<AppContainer
                component={require(./components/root)} props={{store}} />
...

Is the require part necessary? You can't just include that already imported Root component for this? I have been trying to be in the habit of imports vs requires but I notice this require seems to be necessary, maybe to re-instantiate the component?

Thanks for all the hard work, @gaearon!

gaearon commented 8 years ago

1) Does anyone have a complicated example yet using redux/redux-react-router? Something like this non-working example that works would be great:

Just move <Provider>...</Provider> into a separate component called Root in another file that looks like

export default Root() {
  return <Provider>...</Provider>
}

Then you can use <AppContainer><Root /></AppContainer> in both places.

Is the require part necessary? You can't just include that already imported Root component for this? I have been trying to be in the habit of imports vs requires but I notice this require seems to be necessary, maybe to re-instantiate the component?

It is necessary in Webpack 1.x because the Root component will point to the old version even after a hot update. require()ing it again retrieves the new version.

In Webpack 2, this is unnecessary because Root binding is updated automatically. So you can just use Root again.

framerate commented 8 years ago

Thanks @gaearon. So what am I missing here?

Passing "component" prop to is deprecated. Replace

It's been a long day so apologies if it's simple, but if this is deprecated, how do I do:

<AppContainer component={require(./components/root)} props={{store}} />

Is this going to be acceptable behavior?

let X = require('./components/root.jsx').default;
        ReactDOM.render(
            <AppContainer>
                <X />
            </AppContainer>,
                document.getElementById('container'));
    });
CrocoDillon commented 8 years ago

@framerate yes, and that way you can also use props normally. <X store={ store } />.

leshow commented 8 years ago

@framerate I have a "complicated" boilerplate for this I made last night. It uses react-router/react-router-redux and a laundry list of other things. I mashed together the minimal boilerplate example with react-slingshot

https://github.com/leshow/react-hot-loader-boilerplate

dferber90 commented 8 years ago

I created a compatibility table of hot-loadable components. It contains different kinds of components together with react-router. Maybe this is helpful to somebody else wondering why it sometimes works and sometimes doesn't.

The result for react-hot-loader/babel:

Component Hot Loading
Sync
SyncIndexRouteComponent
SyncRouteComponent
SyncRouteComponentChild
SyncNonExportedComponent
SyncReferenceModifiedComponent
SyncAssignedComponent
Aync
AsyncIndexRouteComponent
AsyncRouteComponent
AsyncRouteComponentChild

After manually remounting the components by changing routes back and forth all hot-updates were applied to components which ignored updates initially (marked with —).

Legend:

Dependencies:

The repository can be found here: https://github.com/dferber90/react-hot-boilerplate.

@gaearon Is this how it is supposed to work at the moment or did I mess up the setup?

I also discovered that components in async routes are hot-loaded correctly when they are included into the application somewhere else directly (e.g. adding import './AsyncRouteComponent' to App.js in the example repo would make it hot-load correctly).

gaearon commented 8 years ago

@dferber90

Thank you very much for creating a sample app! I added a link to it from https://github.com/gaearon/react-hot-loader/issues/288. Please feel free to help out!

dferber90 commented 8 years ago

For anybody interested, I have managed to set up react-hot-loader to work with react-router async routes (and redux) by manually adding some workarounds. These workaround will hopefully become unnecessary once react-hot-loader v3 is out and react-router is upgraded. Use with caution until then :)

1. Avoid breaking react-hot-loader

Make sure all your components are defined with const. This is to avoid changing the reference of a component before the export which would break react-hot-loader. More about this in https://github.com/dferber90/react-hot-boilerplate.

2. Avoid react-router warning messages

Create a file referentially-equal-root-route which only contains export default {}. Import that file where your routes are defined and use

import referenctiallyEqualRootRoute from './referentially-equal-root-route'
const routes = Object.assign(referenctiallyEqualRootRoute, {/* your actual routes */})

// ...
render () { return <Router routes={routes} /> }
// ..

This ensures the root route has the same reference even across hot-loads and therefore avoids the warning message. This workaround can be removed once react-router accepts changing props.

I'm not sure of the consequences this approach has for onEnter onLeave hooks being fired, so use with caution. This step is not really required for hot-loading components of async routes (I think).

3. Enable hot-loading components of async routes

This is to enable hot-loading components of async routes. In whatever component is wrapped in AppContainer, import the code-split points into the main application (in development).

Given you have this somewhere in your app

getChildRoutes (location, cb) {
    require.ensure([], require =>
      cb(null, require('./async').childRoutes))
  },

Add this to your Root component

if (process.env.NODE_ENV !== 'production') {
  // ...
  require('./async') // path to the same `async` file as above
}

When the main bundle contains the code-split components hot-loading them will work. Since this should only be the case in development, we only include them in dev mode.

gaearon commented 8 years ago

Make sure all your components are defined with const. This is to avoid changing the reference of a component before the export which would break react-hot-loader.

Woah, good gotcha. We should definitely document this. It’s a bit of an unfortunate limitation but I don’t see this as a big issue yet. I filed https://github.com/gaearon/react-hot-loader/issues/295.

When the main bundle contains the code-split components hot-loading them will work. Since this should only be the case in development, we only include them in dev mode.

I wonder if this is intentional or not. Is this a bug in Webpack? Why doesn’t it propagate hot updates to async parents? Do you think it’s worth to raise an issue?

dferber90 commented 8 years ago

Woah, good gotcha

Thank you :)

I wonder if this is intentional or not. Is this a bug in Webpack? Why doesn’t it propagate hot updates to async parents? Do you think it’s worth to raise an issue?

Well, the updates seem to arrive. The app gets rerendered from the root, through the module.hot.accept callback.

I have no idea why this fixes hot-loading, I stumbled upon this workaround while debugging. I'm not familiar with webpack internals so I can't judge where the culprit lies :(

framerate commented 8 years ago

I'm working on converting a rather complicated electron package over (and rewriting my webpack config as I go).

Is this immediately indicative of something? If I switch away from only-dev-server it forces a full refresh but I thought the Maximum call size exceeded might be indicative of a common mistake I missed while upgrading to hmr3. Any suggestions appreciated!

[WDS] App updated. Recompiling...
[WDS] Warnings while compiling.
./~/encoding/lib/iconv-loader.js
Critical dependencies:
9:12-34 the request of a dependency is an expression
 @ ./~/encoding/lib/iconv-loader.js 9:12-34
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] The following modules couldn't be hot updated: (They would need a full reload!)
[HMR]  - 856
[HMR] Nothing hot updated.
[HMR] App is up to date.
Uncaught RangeError: Maximum call stack size exceeded
0x80 commented 8 years ago

@dferber90 thanks for your example!

Do you (or anyone here) know if it is safe to create singletons like the application store by putting an instance on export default like you do in store.js? I couldn't find any definite answer on this. I thought hot reloading could maybe mess things up but I'd love to have a confirmation that this is ok...