Skip to content
This repository has been archived by the owner on Jun 15, 2023. It is now read-only.

Investigate the ability to work with Redux hot reload #1097

Closed
paulmillr opened this issue Jan 6, 2016 · 6 comments
Closed

Investigate the ability to work with Redux hot reload #1097

paulmillr opened this issue Jan 6, 2016 · 6 comments

Comments

@paulmillr
Copy link
Contributor

No description provided.

@goshacmd
Copy link
Contributor

The scope is actually broader than just Redux — it can apply at anything made with React (maybe even Exim, although not sure about this one).

Here's a few approaches:

1. Naive

When a JS file changes, reload the javascript files instead of refreshing the page. With this, we'll also want to flush the local module cache. The app modules themselves (probably only initialize?) will have to handle reload themselves. They'd probably have to store some important state in window, and check for presence of previous state.

In case of React, that would mean wrapping every component with react-proxy, plus with Redux specifically, caching store and replacing reducers with a fresher version.

The following changes will be required:

  • auto-reload-brunch: reload scripts without doing page reload
  • commonjs-require-definition: API to clear caches

And here's an example of what initialize of our redux starter will look like:

import ReactDOM from 'react-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import counterApp from './reducers';
import App from 'components/App';

import createProxy from 'react-proxy';
import deepForceUpdate from 'react-deep-force-update';

if (!window.store) {
  window.store = createStore(counterApp, 0);
} else {
  window.store.replaceReducer(counterApp);
}

var Root = React.createClass({
  render() {
    return <Provider store={window.store}>
      <App />
    </Provider>
  }
});

var first;

if (!window.proxy) {
  window.proxy = createProxy(Root);
  first = true;
} else {
  window.proxy.update(Root);
}

var Proxy = window.proxy.get();

setTimeout(function() {
  if (first) {
    window.rd = ReactDOM.render(
      <Proxy />,
      document.querySelector('#app')
    );
  } else {
    deepForceUpdate(rd);
  }
}, 250);

2. A better Naive

Targeting React specifically, it can be seen that wrapping into react-proxy is required for every component and is mundane. This can be automated by creating a Babel React transform plugin

LiveReactload, a live React reload for Browserfy is using a custom react transform plugin to do that, for example — https://github.com/milankinen/livereactload/blob/master/src/babel-transform/main.js

The following changes will be required:

  • auto-reload-brunch: reload scripts without doing page reload
  • commonjs-require-definition: API to clear caches
  • create a react transform to wrap into react-proxy

And here's an example of what initialize of our redux starter could look like:

import ReactDOM from 'react-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import counterApp from './reducers';
import App from 'components/App';

if (!window.store) {
  window.store = createStore(counterApp, 0);
} else {
  window.store.replaceReducer(counterApp);
}

var Root = React.createClass({
  render() {
    return <Provider store={window.store}>
      <App />
    </Provider>
  }
});

setTimeout(function() {
  if (window.rd) return;
  window.rd = ReactDOM.render(
    <Root />,
    document.querySelector('#app')
  );
}, 250);

3. Hot Module Replacement (HMR) API

A more advanced solution could be to implement Webpack's Hot Module Replacement API

It could allow to, for example, pass that state between old and new version of a module without using window to store state.

The following changes will be required:

  • auto-reload-brunch: reload scripts without doing page reload
  • React specific: create a react transform to wrap into react-proxy
  • deppack and commonjs-require-definition — expose module.hot; have more complicated module logic that will have to figure out hot dependency listeners at runtime; perform smarter module replacements

On the pro's side, it allows for more flexibility in regards to replacements.

The API, however, has 11 entry points and could very well complicate deppack and commonjs-require-definition greatly.

And here's an example of what initialize of our redux starter could look like:

import ReactDOM from 'react-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import counterApp from './reducers';
import App from 'components/App';

var store = createStore(counterApp, 0);

// this, instead of window.store + checks
if (module.hot) {
  module.hot.accept('./reducers', function() {
    counterApp = require('./reducers');
    store.replaceReducer(counterApp);
  });
}

var Root = React.createClass({
  render() {
    return <Provider store={store}>
      <App />
    </Provider>
  }
});

setTimeout(function() {
  ReactDOM.render(
    <Root />,
    document.querySelector('#app')
  );
}, 250);

Conclusion

These can be seen as gradual "levels" of live JS reloading, not as totally different approaches.

No.1 and no.2 are more or less easy to implement right here, right now and provide an easier time developing React apps without reloads (there could be some limitations as to what React Proxy can't handle as well as there could be sporadic, app-dependent issue related to state, in which case a reload will be required).

Then, with that baseline already established, at some later point, we could evaluate exactly how easy/hard will it be to bring HMR to Brunch, to allow for more fine-grained control of module replacements and to avoid storing state globally.

cc @paulmillr

@adrianmcli
Copy link

@goshakkk You wrote a really good summary of our options going forward. I was wondering though, should the discussion be here in the core brunch repo or should it be in one of the React-based skeletons?

@goshacmd
Copy link
Contributor

goshacmd commented Mar 2, 2016

@adrianmc core seems like a better fit, if only because the changes are required to core itself/other plugins, and also because there are at least 3 official react-based skeletons (react, exim, redux)

@paulmillr
Copy link
Contributor Author

  • auto-reload-brunch: reload scripts without doing page reload
  • commonjs-require-definition: API to clear caches

those steps are needed in any case. let's start, I guess

@goshacmd
Copy link
Contributor

Created a sample React app with Brunch to showcase the adopted react-transform babel plugin that automatically wraps React components into proxies — https://github.com/goshakkk/brunch-livejs-reload-stage2 built on top of https://github.com/brunch/react-livejs

initialize.js looks like this now:

import ReactDOM from 'react-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import counterApp from './reducers';
import App from 'components/App';

// detect if we're loading for the first time or reloading
if (!window.store) {
  window.store = createStore(counterApp, 0);
} else {
  window.store.replaceReducer(counterApp);
}

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <Provider store={window.store}>
      <App />
    </Provider>,
    document.querySelector('#app')
  );
});

which is definitely an improvement compared to something like this:

import ReactDOM from 'react-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import counterApp from './reducers';
import App from 'components/App';

import {createProxy} from 'react-proxy';
import deepForceUpdate from 'react-deep-force-update';

if (!window.store) {
  window.store = createStore(counterApp, 0);
} else {
  window.store.replaceReducer(counterApp);
}

var Root = React.createClass({
  render() {
    return <Provider store={window.store}>
      <App />
    </Provider>
  }
});

var first;

if (!window.proxy) {
  window.proxy = createProxy(Root);
  first = true;
} else {
  window.proxy.update(Root);
}

var Proxy = window.proxy.get();

setTimeout(function() {
  if (first) {
    window.rd = ReactDOM.render(
      <Proxy />,
      document.querySelector('#app')
    );
  } else {
    deepForceUpdate(rd);
  }
}, 250);

The next thing to research/implement is probably Hot Module Replacement API.

@goshacmd
Copy link
Contributor

Now that we have https://github.com/brunch/hmr-brunch, can you close this? @paulmillr

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

No branches or pull requests

3 participants