rt2zz / redux-persist

persist and rehydrate a redux store
MIT License
12.93k stars 865 forks source link

redux persist with redux saga integration problem #794

Open majidln opened 6 years ago

majidln commented 6 years ago

I use redux persist for persist redux data. I persist user info in redux persist and its so cool. but there is a big problem when I use redux saga, so redux persist cannot persist saga data

my config store like below:

import { createStore, applyMiddleware } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import createEncryptor from 'redux-persist-transform-encrypt';
import createSagaMiddleware from 'redux-saga';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web and AsyncStorage for react-native
import { setHeader } from './api';
import rootReducer from './../reducers/index';
import rootSaga from '../sagas/index';

// create the saga middleware
const sagaMiddleware = createSagaMiddleware();

const encryptor = createEncryptor({
  secretKey: '*********',
  onError(error) {
    console.log('Encryptor error', error);
    // Handle the error.
  },
});

const persistConfig = {
  key: 'root',
  storage,
  transforms: [encryptor],
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export default () => {
  const store = createStore(persistedReducer, applyMiddleware(sagaMiddleware));
  const persistor = persistStore(store);

  //run the saga
  sagaMiddleware.run(rootSaga);

  // add listener for store for listen to user auth change and set axios headers
  store.subscribe(() => setHeader(store.getState().user && store.getState().user.auth));

  return { store, persistor };
};

my saga code for get list from api

function* fetchPage(action) {
  try {
    const response = yield apiGet(action.name);  // get one page of my account list for example
    yield put({
      type: 'FETCHING_DATA_SUCCESS', data: response.data, name: action.name,
    });
  } catch (e) {
    yield put({ type: 'FETCHING_DATA_FAILURE', message: e.message, name: action.name });
  }
}

export const saga = [
  takeEvery('FETCHING_DATA', fetchPage),
];

Update in 14 November 2018

I add below case in my reducer and done it

case 'persist/REHYDRATE':
    return action.payload || {}
JulianKingman commented 6 years ago

@majidln any luck with this issue? I'm having some data inconsistencies that I think relate to this issue.

hammadzz commented 5 years ago

@majidln @JulianKingman has anyone come up with a correct implementation of redux-persist with redux-saga?

klis87 commented 5 years ago

@hammadzz I use both libraries extensively, but I am not sure why they could conflict each other? I dont have any issues despite the fact I use them both

JulianKingman commented 5 years ago

@hammadzz I don't remember exactly how I solved it, but I think it may have been a race condition. Make sure you set up a listener for when state is rehydrated before updating allowing app interactions to update state. Looks like my loading screen navigates to the app after it's rehydrated, but there are other ways to do it.

majidln commented 5 years ago

@majidln @JulianKingman has anyone come up with a correct implementation of redux-persist with redux-saga?

I do it, there is not a problem and it works fine

hammadzz commented 5 years ago

@klis87 any boilerplate? I am using the example above not sure if it will work properly. Trying it out once I fix something else breaking my app.

I am migrating from redux-storage, it had an action to watch for when data is persisted so you could subscribe to it with a watcher. Is that possible in redux-persist?

I had a sagas/init.js that was the very first saga that let you do neat stuff like so:

import { take, fork, put, call } from 'redux-saga/effects'
import { LOAD } from 'redux-storage'; // LOAD - action type when redux persists data

function *watchReduxLoadFromDisk() {
  while(true) {
    const { payload } = yield take(LOAD);  //Subscribe to when app finishes loading
    try {
      if (payload.user.token) {
        // set Authorization header to loggedin user token
        yield call(api.setHeader, 'Authorization', `JWT ${payload.user.token}`)
        // Calls to refresh logged in user's profiles
    yield put({type: actions.FETCH_ME})
        // request for chat token
        yield put({type: actions.CHAT_GET_TOKEN})
      } else {
        // remove authorization header
        yield call(api.deleteHeader, 'Authorization')
      }
    } catch (err) {
      // remove authorization header
      yield call(api.deleteHeader, 'Authorization')
    }
    yield call(SplashScreen.hide)
  }
}
hammadzz commented 5 years ago

I think I found that action

import { REHYDRATE } from 'redux-persist/lib/constants'
klis87 commented 5 years ago

@hammadzz I dont really have a boilerplate as I used much more stuff that those 2 libs, but generally I just used readme of both projects as a start and it worked out, as those libraries don't get into each other's way.

Probably people have bugs because they didn't wait for REHYDRATE action, like you did. Personally I didn't do this though, as I persist only data which are used by React component, and this is what PersistGate is for, my components won't get rendered until rehydration is ready so I dont have any race condition bugs this way

Nantris commented 5 years ago

Just to spell it out for anyone confused on this:


import { all, fork, take } from 'redux-saga/effects';
import { REHYDRATE } from 'redux-persist/lib/constants';
import { mySagaA, mySagaB, mySagaC } from './mySagas';

function* rootSaga() {
  console.log("Waiting for rehydration")
  yield take(REHYDRATE); // Wait for rehydrate to prevent sagas from running with empty store
  console.log("Rehydrated")
  yield all([
    fork(mySagaA),
    fork(mySagaB),
    fork(mySagaC),
  ]);
}
daniel-centore commented 2 years ago

Waiting for rehydration still wasn't quite long enough for it to be ready for me for some reason. In order to be equivalent to PersistGate I needed to do this ugly hack...

    while (!persistor.getState().bootstrapped) {
        yield call(delay, 1);
    }

Do let me know if there's something better which could be done here!

Nantris commented 2 years ago

Unfortunately I found that what worked in development failed in PROD for us when the dataset to rehydrate with was large enough. Unfortunately I haven't got any tips because we ended up writing our own function to load/save the entire app state to a file without using redux-persist. It's not very efficient, but it does work.

Still, my last attempt was nearly 3 years ago - so just because I never got it working without dropping redux-persist, doesn't mean there's not a better solution by now.