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

Move safety checks inside combineReducer to first invocation #717

Closed
gaearon opened this issue Sep 13, 2015 · 16 comments
Closed

Move safety checks inside combineReducer to first invocation #717

gaearon opened this issue Sep 13, 2015 · 16 comments

Comments

@gaearon
Copy link
Contributor

gaearon commented Sep 13, 2015

Currently we make a bunch of invariant tests inside combineReducers() to make sure it's not being misused in common ways. These tests currently occur at the combineReducers() call time.

This makes Redux DevTools unable to catch the errors inside reducers, as they often trigger those invariants to blow up during module definition, and abort hot reloading. Instead, I propose to move the tests to the first real invocation of the combined reducer. In most cases that would be createStore call because it causes an implicit initial dispatch. This way Redux DevTools can catch and display such errors.

Shall we do that? The code would change to be like

  var reducerSanityVerified;
  var stateShapeVerified;

  // don't do it here

  return function combination(state = defaultState, action) {
    if (!reducerSanityVerified) {
      Object.keys(finalReducers).forEach(key => {
        // do that stuff

screen shot 2015-09-13 at 07 09 31

@gaearon
Copy link
Contributor Author

gaearon commented Sep 13, 2015

Another possible alternative is to create errors and console.error() them ASAP during combineReducers() call, but throw during first dispatch. This way we both have a nice stack and keep DevTools happy.

@gaearon
Copy link
Contributor Author

gaearon commented Sep 16, 2015

Would anybody like to work on this?
@ellbee, do you think you might have the time?

@ellbee
Copy link
Contributor

ellbee commented Sep 16, 2015

Sure, I'll be able to take a look in the next couple of days.

@ellbee
Copy link
Contributor

ellbee commented Sep 18, 2015

@gaearon I have been working on this, but I am new to the dark arts of HMR and I have a question.

In the following module taken from the counter example, I think delaying the sanity check until the dispatch of the @@init action will still blow up the module definition because the init action is getting dispatched either inside createStore or replaceReducer. This module is going to get re-evaluated too when a reducer changes I think?

import React, { Component } from 'react';
import CounterApp from './CounterApp';
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import { devTools, persistState } from 'redux-devtools';
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import * as reducers from '../reducers';

const finalCreateStore = compose(
  applyMiddleware(thunk),
  devTools(),
  persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
)(createStore);

const reducer = combineReducers(reducers);
const store = finalCreateStore(reducer);

if (module.hot) {
  module.hot.accept('../reducers', () =>
    store.replaceReducer(combineReducers(require('../reducers')))
  );
}

export default class App extends Component {
  render() {
    return (
      <div>
        <Provider store={store}>
          {() => <CounterApp />}
        </Provider>
        <DebugPanel top right bottom>
          <DevTools store={store}
                    monitor={LogMonitor}
                    visibleOnLoad={true} />
        </DebugPanel>
      </div>
    );
  }
}

I've got a couple of potential ideas to get around this if it is indeed a problem, just wanted to check that I am not misunderstanding how the HMR works.

@gaearon
Copy link
Contributor Author

gaearon commented Sep 18, 2015

It will below up the first time you launch the app but I don't think it's a big deal. It's iteration that I'm concerned about (changing reducers or adding new ones). In case of iteration only the reducer file and replaceReducer() call will be evaluated.

@ellbee
Copy link
Contributor

ellbee commented Sep 18, 2015

Ah, thanks! That makes total sense. I should read my error messages. The error thrown on iteration is caused by the Connect already being hooked up and mapStateToProps being triggered by the dispatch in replaceReducers with undefined state.

@gaearon
Copy link
Contributor Author

gaearon commented Sep 18, 2015

Yeah—that's a different thing, reduxjs/redux-devtools#106

@ellbee
Copy link
Contributor

ellbee commented Sep 18, 2015

So to achieve our goal that devtools issue will need to be addressed too I guess. Having this error thrown from the dispatch in replaceReducers is resulting in [HMR] Cannot apply update. Need to do a full reload!, so devtools isn't displaying the errors like we want.

@gaearon
Copy link
Contributor Author

gaearon commented Sep 18, 2015

Yes, but these are two separate issues. When I tested, fixing them both locally fixed the problem.

@ellbee
Copy link
Contributor

ellbee commented Sep 19, 2015

Yes, agreed.

gaearon added a commit that referenced this issue Sep 23, 2015
@gaearon
Copy link
Contributor Author

gaearon commented Sep 24, 2015

Done in #717.

@gaearon gaearon closed this as completed Sep 24, 2015
@gaearon
Copy link
Contributor Author

gaearon commented Sep 25, 2015

Released in 3.0.1.

@gaearon gaearon reopened this Sep 26, 2015
@gaearon
Copy link
Contributor Author

gaearon commented Sep 26, 2015

There is still a problem with the scenario I described above. If the reducer throws, then the reducer sanity check itself will throw. In other words,

  Object.keys(finalReducers).forEach(key => {
    var reducer = finalReducers[key];
    if (!sanityError && typeof reducer(undefined, { type: ActionTypes.INIT }) === 'undefined') {

See that typeof reducer(...) call? It will blow up if reducer throws, blowing up combineReducers anyway. I'd say that the way sanity check should be changed is the following:

  • Let sanity check throw
  • Catch the error outside Object.keys(...) and store as sanityError
  • Like now, throw it on first dispatch

@ellbee Can you look into this?

@ellbee
Copy link
Contributor

ellbee commented Sep 26, 2015

Ah yes, I see. Quick fix attempt here #807

@gaearon
Copy link
Contributor Author

gaearon commented Sep 26, 2015

Fixed via #807.

@gaearon gaearon closed this as completed Sep 26, 2015
@gaearon
Copy link
Contributor Author

gaearon commented Sep 26, 2015

Final fix in 3.0.2, confirmed as working.
Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants