reduxjs / react-redux

Official React bindings for Redux

Home Page:https://react-redux.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

<Provider> does not support changing `store` on the fly.

remojansen opened this issue · comments

I'm trying to use redux with react-redux react-router-redux and redux-immutable and I get an error when the initial @@router/LOCATION_CHANGE action is triggered:

action @ 14:19:07.625 @@router/LOCATION_CHANGE 
%c prev state color: #9E9E9E; font-weight: bold Map { "repos": Map { "loading": false, "reposCount": 0 }, "users": Map { "loading": false, "usersCount": 0 }, "router": Map { "locationBeforeTransitions": null } }
%c action color: #03A9F4; font-weight: bold { type: '@@router/LOCATION_CHANGE',
  payload: 
   { pathname: 'blank',
     search: '',
     hash: '',
     state: null,
     action: 'POP',
     key: '5b05pd',
     query: {},
     '$searchBase': { search: '', searchBase: '' } } }
%c next state color: #4CAF50; font-weight: bold Map { "repos": Map { "loading": false, "reposCount": 0 }, "users": Map { "loading": false, "usersCount": 0 }, "router": Map { "locationBeforeTransitions": Map { "pathname": "blank", "search": "", "hash": "", "state": null, "action": "POP", "key": "5b05pd", "query": Map {}, "$searchBase": Map { "search": "", "searchBase": "" } } } }
—— log end ——
<Provider> does not support changing `store` on the fly. It is most likely that you see this error because you updated to Redux 2.x and React Redux 2.x which no longer hot reload reducers automatically. See https://github.com/reactjs/react-redux/releases/tag/v2.0.0 for the migration instructions.
Warning: [react-router] You cannot change <Router history>; it will be ignored

My Root component looks as follows:

class Root extends React.Component<RootComponentProps, void> {
  public render() {
    const { store, history, routes } = this.props;
    return (
      <Provider store={store}>
        <div>
          <Router history={history}>
            {routes}
          </Router>
          <DevTools />
        </div>
      </Provider>
    );
  }
}

I have implemented a custom routerReducer to work with immutable, as explained in the react-router-redux docs:

const initialRouterReducerState = Immutable.fromJS({
    locationBeforeTransitions: null
});

let routerReducer = (state = initialRouterReducerState, action: any) => {
    if (action.type === LOCATION_CHANGE) {
        return state.merge({
            locationBeforeTransitions: action.payload
        });
    }
    return state;
};

This is my dev configureStore:

function configureStore(middlewares: Redux.Middleware[], rootReducer: Redux.Reducer, initialState: any): Redux.Store {
    const store = createStore(
        rootReducer,
        initialState,
        compose(
            applyMiddleware(...middlewares),
            DevTools.instrument()
        )
    );

    return store;
}

And my initial rendering:

  // ...
   const store = configureStore(middlewares, rootReducer, immutableInitialState);

    const history = syncHistoryWithStore(browserHistory, store, {
        selectLocationState: (state: any) => state.get("routing").toJS()
    });

    // Render Root coponent
    render(
        <Root store={store} history={history} routes={routes} />,
        document.getElementById(container)
    );

I've been trying all evening multiple things but I have not been able to figure out what is the problem 😞

commented

Have you had a chance to look at the source of <Provider>? It’s smaller than your example code 😉 .
The line in question executes when <Provider> receives a different store instance as a prop.

This likely means

const store = configureStore(middlewares, rootReducer, immutableInitialState);

executes not once, as you probably intend, but multiple times, so two stores are created rather than one. You may set a debugger statement there and see why it happens. I hope this helps!

Thanks! I have found the issue, I was running my unit tests sharing some stubs and the same instances were shared between multiple unit tests.

commented

👍

@gaearon

executes not once, as you probably intend, but multiple times, so two stores are created rather than one.

I am really not understanding how this is happening. Has there been any recent updates on this matter that may be helpful, because I have been tinkering the heck out of this and am not getting anywhere.

Thanks in advance - I know this is probably a burned out topic you've covered too many times.

Here's my source:

configureStore.js

...
const enhancer = composeEnhancers(
    applyMiddleware(thunk, router, logger),
    autoRehydrate()
);

export default function configureStore(initialState: Object | void) {
    
    const store = createStore(rootReducer, initialState, enhancer);

    persistStore(store, {storage: localForage}).purge()

    if (module.hot) {
        module.hot.accept('../reducers', () => {
            const nextRootReducer = require('../reducers/index').default();
            store.replaceReducer(nextRootReducer);
        });
    }

  return store;
}

Root.js

...
import configureStore from '../store/configureStore';
const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store);

export default () => (
    <Connector horizon={horizon}>
        <Provider store={store}>
            <Router history={history} routes={routes} />
        </Provider>
    </Connector>
);

@ddaaggeett : you probably need to move your const store = configureStore() out of Root.js, and into your application's entry point file. What's probably happening is that Root.js is getting hot reloaded, and it's generating a new store instance in the process.

I've got a working example of using the plain HMR API in my blog post Practical Redux, Part 3: Project Setup.

@markerikson

Here's the entry point of my application:
entry.js

...
import Root from './containers/Root';

const rootElement = document.getElementById('root');

if (module.hot) {
  module.hot.accept();
}

ReactDOM.render(
  <AppContainer>
    <Root />
  </AppContainer>,
  rootElement
);

if (module.hot) {
  module.hot.accept('./containers/Root', () => {
    const RootEle = require('./containers/Root').default;
    ReactDOM.render(
      <AppContainer>
          <RootEle />
      </AppContainer>,
      rootElement
    );
  });
}

Are you saying, place <Provider store={store} /> from Root.js just inside the <AppContainer /> of entry.js, like so:

...
<AppContainer>
    <Provider store={store} >
          <Root />
    </Provider>
</AppContainer>
...

@ddaaggeett : along that line, yeah. The key is to not call configureStore() inside of Root, which also generally means not rendering Provider in there either. See https://github.com/markerikson/project-minimek/blob/master/src/index.js for an example of how I do it in my sample application.

@markerikson - thanks for the examples

I see how the store is to be rendered a single time in the higher component.

entry.js:

...
ReactDOM.render(
    <AppContainer>
        <Connector horizon={horizon}>
            <Provider store={store}>
                <Root />
            </Provider>
        </Connector>
    </AppContainer>,
    rootElement
);

but now, how is the store used to create history which is fed to { Router } from 'react-router'?

Root.js:

...
export default ({store}) => {
    const history = syncHistoryWithStore(hashHistory, {store});
    return (
        <Router history={history} routes={routes} />
    )
}

this is obviously incorrect and results in the error:

Uncaught TypeError: t.getState is not a function

or, without passing { store } as an argument:

Uncaught ReferenceError: store is not defined

Ah... I don't use routing myself, but I'm pretty sure you don't want to call that syncHistoryWithStore() inside of a component's render() method. That should also be done just once, as part of the app initialization process.

i've posted a more detailed cover here

For resolving this error you need put store = createStore(reducers) on top level of your containers Now your hot-loader triggers creating store
Here is example.
Also <Devtools /> can be reason of problem, remove it for testing

what about singleton?

const configureStore = (function () {
  let store
  return {
    create () {
      if (!store) {
        store = createStore()
      }
      return store
    }
  }
})()

const store = configureStore.create()