rt2zz / redux-persist

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

[HELP] Redux-persist with immutable map not persisting state changes #790

Open kg912 opened 6 years ago

kg912 commented 6 years ago

Hi,

I am using redux-persist with my entire state tree being stored as an immutable map as shown in my store.js file code below.

import {persistReducer, persistStore} from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel1 from 'redux-persist/lib/stateReconciler/autoMergeLevel1'
import { Record } from 'immutable';
import immutableTransform from 'redux-persist-transform-immutable';
const history = createHistory();
const middlewares = [thunk, sagaMiddleware, routeMiddleware, ReduxPromise];
// some imports omitted in the interest of brevity.
let persistor;

const MyRecord = Record({
    App : {},
    Auth : {},
    Box  : {},
    Calendar : {},
    Cards : {},
    Contacts : {},
    Course : {},
    DynamicChartComponent : {},
    Ecommerce : {},
    LanguageSwitcher : {},
    Mails : {},
    Notes : {},
    ThemeSwitcher : {},
    Todos : {},
    User : {},
    YoutubeSearch : {},
    githubSearch : {}
}, 'MyRecord')

const persistConfig = {
    transforms: [immutableTransform({records: [MyRecord]})],
    key: 'root',
    storage,
    stateReconciler: autoMergeLevel1
}

const persistedReducer = persistReducer(persistConfig, combineReducers({
    ...reducers,
    router: routerReducer
}));

const loggerMiddleware = createLogger({
    predicate: () => process.env.NODE_ENV === 'development',
});
middlewares.push(loggerMiddleware);

export default () => {
    let store = createStore(persistedReducer, compose(applyMiddleware(...middlewares), reduxReset()))
    sagaMiddleware.run(rootSaga);
    persistor = persistStore(store);
    return { store, history, persistor }
}

export function getPersistor() {
    return persistor;
}

My Problem is that even after I make changes to the state tree (which I have confirmed, do in fact take place as they should, using redux logger), the changes aren't shown in my application on refresh despite the fact that I call persistor.flush() like this:

export function* materialUpdate() {
    yield takeEvery(actions.COURSE_MAT_UPDATE, function*() {
        let secs = 2;
        while (secs > 0) {
            yield call(delay, 1000);
            secs--;
        }
        // const flushData = persistor => persistor.flush();
        // const persistor = yield call(getPersistor);
        yield put(getPersistor().flush);
    });
}

right after the state change (Also verified that that the flush action fires). I've tried everything and I would really appreciate any advice whatsoever, on the matter. Thanks in advance.

tommyalvarez commented 6 years ago

@kg912 you shouldn't need to manually call persistor.flush(). On redux state change, if the reducer key is whitelisted, it should automagically store in localStorage the reducer state. Did you check using redux dev tools that the ui user state actually change after dispatching an action in the reducer ?

kg912 commented 6 years ago

Hi tommyalvarez, I have verified that the component level state does change as I update my application level (Redux) state. My problem is that the initial state is being persisted on refresh instead of the newer application state. My root reducer takes in the following differrent reducers

import Auth from './auth/reducer';
import App from './app/reducer';
import Mails from './mail/reducer';
import Course from './course/reducer';

export default {
  Auth,
  App,
  Mails,
  Course
};

and each reducer stores its state in an immutable map like this:

import courseActions from './actions';
import { Map } from 'immutable';
const course_data = {};
const { COURSE_DATA, COURSE_UPDATE, COURSE_MAT_UPDATE, REMOVED_MATERIAL, RETRIEVE_COURSE, COURSE_ASSIGN_UPDATE} = courseActions;

const initState = new Map({
    course_data,
});

export default function(state = initState, action) {
    switch(action.type) {
        case COURSE_UPDATE:
            const { courseId, newCourse } = action.payload;
            let data = state.get('course_data');
            data[courseId] = newCourse;
            state.set('course_data', data);
            return state.set('course_data', data);
            break;

        case COURSE_DATA:
            const { course, id } = action;
            let old_state = state.get('course_data');
            return state.set('course_data', { ...old_state, [id]: course});
            break;

        case COURSE_MAT_UPDATE:
            const { materials, cId} = action;
            let courseData = state.get('course_data');
            courseData[cId].materials = materials;
            return state.set('course_data', courseData);
               }
     }

Do I need to explicitly whitelist all of these reducers? also do I need to pass anything at all to the immutableTransform function?

tommyalvarez commented 6 years ago

@kg912 No, if you want to persist all your reducers states you should not specify either whitelist nor blacklist arguments. However, i would recommend whitelisting so only the parts of the state that you really need persisted, are actually persisted. Otherwise you could end up with a heavy persisted state into localStorage. That aside, after state change, go check in your chrome console Application tab, if in the localStorage your data is correctly persisted.

NOTICE: I'm seeing in your code that you are using combineReducers. In redux-persist v5, you need to use persistCombineReducers, though it's a little bit hidden, i think it's not in the docs but i read it up on a blog on medium or don't remember quite well but if you google persisteCombineReducers you will see... So, it should be something like this:

import { persistCombineReducers } from 'redux-persist'
const persistedReducer = persistCombineReducers(persistConfig, {
   your_reducers,
   ...asyncReducers
}));

If not working, the probable causes are:

But in conclusion you should neither call persistor.flush() nor manually dispatch any redux-persist reducers actions like persist/REHYDRATE, persist/PERSIST, etc... it works out of the box when correctly setup.

And to your last question, i don't have experience with redux-persist and immutable transforms because i don't use the immutable lib, so i don't know what to answer to that. If you're still banging your head against the wall, try persisting just one of your reducers state without using immutable, make it work and start from there.

samscha commented 6 years ago

I know this is a bit late, but for people in the future having similar problems..

I am assuming what's happening is (this was the case for me at least) is upon refresh, redux-logger shows the correct payload to set during rehydrate, but the next state in logger is showing the reducer's initial state. I noticed this was only happening when I had nested objects (for example, when I was trying to persist strings, it worked fine.

The solution (which is now in the docs, maybe not at the time of the original post) is to use nested persists. I noticed in OP's code:

const persistedReducer = persistReducer(persistConfig, combineReducers({
    ...reducers,
    router: routerReducer
}));

which is similar to what I was doing. I had to explicitly use persistReducer on each reducer in my combineReducers (or at least the ones I was going to persist, such as user object).

So I would propose either doing this programmatically or just manually doing it for each reducer in before combining them. For me, this was:

from

const rootReducer = combineReducers({
  form: formReducer,
  user: userReducer,
});

to

const rootReducer = combineReducers({
  form: formReducer,
  user: persistReducer(persistConfig, userReducer),
});

Similar to OP's code, I had:

const persistedReducer = persistReducer(persistConfig, reducer); 
// this won't work with nested objects in reducers (or at least I don't think)!

where reducer was the rootReducer from my reducer file.

I had to change some imports around but now my user object persists properly! And I didn't have to use hardSet (which was persisting properly, but wouldn't have allowed me to add more "whitelist"/reducers in the future without checking storage somehow or force rewriting storage)!

Here are some snippets of my working code for reference (with some helpful comments). Also my repo:

reduxPersist.js

import storage from 'redux-persist/lib/storage';

const KEY = 'storage-key-here';

export const persistConfig = {
  key: KEY,
  storage,
};

reducers.js

import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';

import { persistConfig } from '../pkgs/reduxPersist';
// had to move this here from reduxPersist.js, which is where it was originally

import formReducer from './form';
import userReducer from './user';

const rootReducer = combineReducers({
  form: formReducer,
  user: persistReducer(persistConfig, userReducer), // put persistReducer here
  // you can also add custom configs for each reducer (e.g. in the nested persists doc)
});

export default rootReducer;

_store.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';

import reducer from './reducers'; // import rootReducer here

const devMiddleware = [];
const middleware = [];

if (process.env.NODE_ENV === 'development') {
  devMiddleware.push(logger);
  // add more dev middleware here
}

middleware.push(thunk);
// add more middleware here

const store = createStore(
  reducer, // don't put persistReducer here
  applyMiddleware(...middleware.concat(devMiddleware)),
);

export default store;

App.js

// other import statements
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';

// some code

import store from '../_store';

// more code

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistStore(store)}>
          {/* more code
        */}
        </PersistGate>
      </Provider>
    );
  }
}

export default App;

hope this helped!

tuankiet-hcmc commented 4 years ago

Thank @samscha, It's worked. You save my day

sanych85 commented 4 years ago

In this case, proposed by @samscha i had only 1 reducer, that work fine. Other not persist state.