rt2zz / redux-persist

persist and rehydrate a redux store
MIT License
12.96k stars 866 forks source link

[v5] HMR-update to reducers through Webpack #490

Open rskaar opened 7 years ago

rskaar commented 7 years ago

I'm having some problems figuring out how to get redux-persist to work while using HMR-updates (or when code-splitting/lazy loading reducers).

My configureStore() contains:

const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = compose(applyMiddleware(...middlewares))(createStore)(persistedReducer);
const persistor = persistStore(store, {}, () => {
  console.log('Store initialized and rehydrated');
});

It works, persistance works and everything is fine.

Then when modifying any of my reducers, I'm trying to get redux-persist to keep working after the HMR-update. I've tried the following callback on update:

const newPersistedReducer = persistReducer(persistConfig, newRootReducer);
store.replaceReducer(newPersistedReducer);
const newPersistor = persistStore(store, {}, () => {
  console.log('This callback is never run'); // Does not get printed because the app is already bootstrapped?
});

I know I'm doing something wrong here. The reducers work as expected, and keeps the state values, but updates/new values after the HMR-update are not stored through redux-persist. A reload leads to rehydrating the values as they were right before the HMR-update.

rt2zz commented 7 years ago

Right, this is not well documented currently nor have I actually done this myself. I think Instead of creating a new persistor, you want to call persistor.persist() on the existing persistor.

From there what should happen:

  1. a new persistoid gets created inside of the persisted reducer https://github.com/rt2zz/redux-persist/blob/v5/src/persistReducer.js#L70
  2. the next time state updates, this conditional passes and triggers persistoid.update https://github.com/rt2zz/redux-persist/blob/v5/src/persistReducer.js#L156-L159

If either of those things is not happening then there is certainly a bug. lmk, I dont have time to set it up right now but will doe my best to review any issues that arise and am open to making changes required to fix this / make it easier.

rskaar commented 7 years ago

Of course! Sorry, I should have tried that! It works as expected with HMR and code splitted reducers.

I've added a manual rehydrate action to my callback to cover the scenarios when I add new reducers (through code splitting).

import getStoredState from 'redux-persist/es/getStoredState';
...

const onUpdateReducers(newRootReducer) => {
  const newPersistedReducer = persistReducer(persistConfig, newRootReducer);
  store.replaceReducer(newPersistedReducer);
  persistor.persist();

  // Rehydrate state to make sure code splitted reducers get stored values
  getStoredState(persistConfig).then((storedState) => {
    store.dispatch({
      type: REHYDRATE,
      key: persistConfig.key,
      err: undefined,
      payload: storedState,
    });
  });
}

I will then handle migrations "outside of" the redux-persist configuration.

Thank you very much for both the module and your reply!

rskaar commented 7 years ago

(or even better, each code splitted module/reducers have their own redux-persist initiation and config...)

stevemao commented 7 years ago

Would be great if docs is updated TBH persist() and REHYDRATE part looks more like a workaround for me. I'd like to know why such boilerplate is needed.

rt2zz commented 7 years ago

ya this whole area needs more exploration. The api was designed to handle this conceptually, but actually the not in the way @rskaar is doing it.

I was thinking codesplit reducers would work as follows:

  1. add persistReducer around just the codesplit reducer (if needed blacklist this reducer in your root persist config)
  2. then call persistor.persist()

When the nested persisted reducer gets the persist action, it will take care of rehydrating itself thus avoiding the getStoredState call.

Lets reopen this and keep the conversation going.

rskaar commented 7 years ago

This issue started when I was testing out ideas for a large scale react/redux app with a lot of codesplit modules using react-universal-component. I wanted to add persisted reducers on both the root-app, and for each module who needed it. When a module is requested, it's reducers (if any) would be added and rehydrated if they have any persisted data. HMR for both the core app, reducers and codesplit modules was also a 'requirement'.

I'm using ReducerRegistry inspired from this example, and the change listener is what is described above as onUpdateReducers.

I started it thinking I needed to use blacklist/whitelist to filter which reducers to persist. This kind of worked, but the rehydrate action then only rehydrated the persisted keys, and did not set the defaults on the keys that was not persisted. I found workarounds on this, but it did not feel 'right'.

Then it hit me that I was doing it wrong, and finally figured out the way @rt2zz describes. I have dedicated (multiple) persisted reducers nested inside the "rootReducer".

{
  'core/user': {
    'persisted': {<initiate redux-persist here and add data to persist>},
    '...': '<other non-persisted keys>',
  },
  'module/some-codesplit-module': {
    'persisted': {<initiate redux-persist here and add data to persist>},
    '...': '<other non-persisted keys>',
  },
}

This is in my opinion a cleaner way, then using blacklist/whitelist-magic. And each module have the possibility to define migrations and other configurations for its own data. My change listener:

reducerRegistry.setChangeListener((reducers) => {
  const newRootReducer = configureReducers(reducers)
  store.replaceReducer(newRootReducer)
  persistor.persist()
})

Works with codesplit, HMR and even HMR on codesplit reducers.

rt2zz commented 7 years ago

great! that is pretty much how I was hoping it might work. Let me know if you find any issues with the approach, if this proves itself out over the next couple weeks Ill look to add explicit instructions to the docs.

djeeg commented 6 years ago

Thanks for the tips in this thread!

My state was only fully persisting on the first page render. Preloading code split reducers (eg hover over links to sub routes) would lose state after reducer rebind and then route change

I also seem to need to flush before rebinding the reducers, is that expected?

    store.isrebindingreducers = true;
    store.persistor.flush()
        .then(function() {
            store.replaceReducer(
                makeRootReducer(
                    {} //initialState
                ));
            store.persistor.persist();
            store.isrebindingreducers = false;
        });