Redux-persist: Redux persist dynamic reducer injection goes into paused state

Created on 26 Jun 2018  路  6Comments  路  Source: rt2zz/redux-persist

Issue:
We are trying to use redux-persist to persist our state tree with dynamically injected reducers. We use create-react-app and react-router to achieve bundle-splitting on a route by route basis.

I've run into multiple different scenarios where redux-persist breaks and does not persist due to unknown issues. To give you a rudimentary example of the code:

We have a function that creates a root reducer:

const asyncReducers: Partial<Reducers> = {};
export const createRootReducer = () => {
  const appReducers: Reducers = {
    ...asyncReducers,
    router: routerReducer,
  };
  return combineReducers(appReducers);
}

Outside of the function we declare a static map of asynchronous reducers we may inject at some point (in this case a counter bundle will inject counter state onto the store state tree)

export type State = {
  readonly counter: CounterState;
  readonly router: RouterState;
} & PersistedState;

Our injectReducer code is a basic implementation that takes the store and checks the reducer you want to inject against the map and validates if the key is available, if so it won't do anything otherwise it will create a new root reducer and replace the reducer.

export const injectReducer = <ReducerState, ReducerKey extends keyof State>(
  store: Store<State>,
  { key, reducer }: ReducerEntry<ReducerState, ReducerKey>,
) => {
  if (typeof asyncReducers[key] !== 'undefined') {
    return;
  }
  asyncReducers[key] = reducer;
  store.replaceReducer(createRootReducer());
};

Now according to your documentation wrapping the above code in HMR's module.hot.accept callback will solve the issues of persistance breaking when injecting a new reducer. However, because our Bundle is controlling this on route load, and the expectation is the reducer be fired and inject the state before the container's first render.

  public async componentWillMount() {
    const { component, reducerEntry, rootEpic } = await this.props.bundleWillLoad();
    if (reducerEntry) {
      injectReducer(store, reducerEntry);
    }
    if (rootEpic) {
      injectEpic(rootEpic);
    }
    this.setState({ component });
  }

What ends up happening is that when this code calls inject reducer (inside the bundle component) the callback of module.hot.accept is never called, but the container tries to bootstrap itself and cannot find state.counter.counter.

Here's a working example of the issue for injecting the reducer, it's persisting the router and if you change the url to / instead of /counter you can see in local storage that it does not update:

https://codesandbox.io/embed/8m4047lz8

I tracked this down in the codebase to the persistor being set to paused = true, and for now I hardcoded and overwrite through an action dispatcher that basically it in an unpaused state.

 store.dispatch({ type: PERSIST });

Basically from what I understand, when you inject a reducer outside of the HMR lifecycle, it seems persistance goes into a paused state because the store's root reducer was changed (??) However I'm not 100% certain on this fact, it took me a while to debug the actual output.

I'm thinking supplying an "UNPAUSE" action wouldn't solve the deeper issue here. However I'm not 100% certain what the deeper issue is exactly, I'd have to dig more through the source-code.

Will try to debug further tomorrow. If you have any idea as to why this may be happening or how to fix it, that would be very much appreciated.

Thank you!

Most helpful comment

You must call persist() after calling store.replaceReducer.

You can do this conveniently if you expose the persistor on your created store. Then, in injectReducer:

store.replaceReducer(createRootReducer());
store.persistor.persist();

All 6 comments

You must call persist() after calling store.replaceReducer.

You can do this conveniently if you expose the persistor on your created store. Then, in injectReducer:

store.replaceReducer(createRootReducer());
store.persistor.persist();

@marcandrews We're using the persist-gate to wrap around the persistor, so on our actual store object, persistor is not available.

Update:
persistor.persist() apparently does the same thing internally that I've hardcoded as a dispatch, i.e:

 persist: () => {
      store.dispatch({ type: PERSIST, register, rehydrate })
    },

but it does pass along the register and rehydrate which may solve the the error that's invoked irregularly
action.rehydrate is not a function

When you create your store, just add persistor to it before you export it:

const store = createStore(
  createReducer(),
  {},
  composeEnhancers(...enhancers)
)

const persistor = persistStore(store)

store.persistor = persistor

export default store

That solves my issue, thanks @marcandrews!

In my case multiple REHYDRATE actions occurred in debug and sometimes in release, it was fix with my PR: #1240

Was this page helpful?
0 / 5 - 0 ratings