Redux-persist: Nested persists only persisting first change

Created on 14 Dec 2017  路  7Comments  路  Source: rt2zz/redux-persist

I'm trying implement nested persists so that I can modify the state as it gets rehydrated. I have tried to follow the example in the README. Below is my code:

import * as localForage from 'localforage';
import { combineReducers } from 'redux';
import { createTransform, persistReducer } from 'redux-persist';

import surveyAnswers, { State as SurveysAnswersState } from './surveyAnswers';
import { default as surveysReducer, State as SurveysState } from './surveys';

export type AppState = {
  surveys: SurveysState,
  surveyAnswers: SurveysAnswersState,
};

const surveysPersistTransforms = createTransform(
  // transform state coming from redux on its way to being serialized and stored
  (state: SurveysState) => {
    return state;
  },
  // transform state coming from storage, on its way to be rehydrated into redux
  (state: any, key: string) => {
    if (key === 'surveys') {
      // ... modify and return state
    }
    return state;
  },
);

const surveysPersistConfig = {
  key: 'surveys',
  storage: localForage,
  transforms: [
    surveysPersistTransforms,
  ],
};

const surveyAnswersPersistConfig = {
  key: 'surveyAnswers',
  storage: localForage,
};

export default combineReducers({
  surveys: persistReducer(surveysPersistConfig, surveysReducer),
  surveyAnswers: persistReducer(surveyAnswersPersistConfig, surveyAnswers),
});

However, when I dispatch more than 1 action and refresh the page I can see that it has only persisted the first change is persisted. If I update my code to the following it will persist all changes, but I lose my manipulation during rehydration :(

import * as localForage from 'localforage';
import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';

import surveyAnswers, { State as SurveysAnswersState } from './surveyAnswers';
import { default as surveysReducer, State as SurveysState } from './surveys';

export type AppState = {
  surveys: SurveysState,
  surveyAnswers: SurveysAnswersState,
};

const combinedReducers = combineReducers<AppState>({
  surveys: surveysReducer,
  surveyAnswers,
});

const config = {
  key: 'app',
  storage: localForage,
};

export default persistReducer(config, combinedReducers);

I create my store as such:

import { applyMiddleware, createStore, Store } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { persistStore } from 'redux-persist';
import reduxThunk from 'redux-thunk';

import rootReducer, { AppState } from '../reducers';

export function configureStore(initialState?: AppState): { persistor: any, store: Store<AppState> } {
  const middleware = [reduxThunk];

  const store = createStore(rootReducer, initialState, composeWithDevTools(
    applyMiddleware(...middleware),
  )) as Store<AppState>;

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

  const persistor = persistStore(store);

  return { persistor, store };
}

Am I doing something wrong or it this a bug?

Most helpful comment

@rt2zz figured out that Object.assign is the culprit in my code, probably the same for @JosephDuffy , given his transform returns the same state object, but mutated. It seems that changing an object without replacing it results at any level of the state results in redux-persist not picking up the change.

I made the test in #711 fail by changing it to incorporate mutation

  let reducer = (state = { counter: 0 }, action) => Object.assign(state, {
    counter: action.type === INCREMENT ? state.counter + 1 : state.counter,
  })

All 7 comments

I'm having the same issue, without any custom transformations:

let persist = (key: string, reducer: Reducer<any>) => persistReducer({
  key,
  storage,
  blacklist: ['actionHistory']
}, reducer)

const reducer = persistCombineReducers({
  key: 'root',
  storage,
  blacklist: ['foo', 'bar']
}, {
  router: routerReducer,
  foo: persist('foo', fooReducer),
  bar: persist('bar', barReducer),
})

This is concerning, I will look into it.

@micimize you do not need to call persistCombineReducers and persistReducer - one or the other. I recommend keep the exact same code and simply remove persistCombineReducers

@JosephDuffy so I wrote a little test for this: https://github.com/rt2zz/redux-persist/pull/711

But it passes. I am not sure what might be going on, can you reproduce this either with a test or a demo project that I can clone?

@rt2zz figured out that Object.assign is the culprit in my code, probably the same for @JosephDuffy , given his transform returns the same state object, but mutated. It seems that changing an object without replacing it results at any level of the state results in redux-persist not picking up the change.

I made the test in #711 fail by changing it to incorporate mutation

  let reducer = (state = { counter: 0 }, action) => Object.assign(state, {
    counter: action.type === INCREMENT ? state.counter + 1 : state.counter,
  })

ah got it. So redux persist relies on an assumption of immutability in a few places, as does the greater redux ecosystem to a large extent. I am not sure we can do anything about this other than better communicate this in the docs.

The comment above is correct -- I must have been mutating the object directly somewhere. I have since moved to Immutable.js and rechecked this issue to find it is no longer present, so it was not an issue with redux-persist!

Thanks, @micimize, you saved my day.
I changed my Object.assign(state, payload) to {...state, ...payload} and it solved the issue.

Was this page helpful?
0 / 5 - 0 ratings