redux-persist icon indicating copy to clipboard operation
redux-persist copied to clipboard

Store rehydrates when close app and reopen even after clearing data

Open connercms opened this issue 7 years ago • 31 comments

I followed Dan Abramov's example on clearing a redux store by dispatching an action that gets handled by a root reducer. My code for that is here

import { persistCombineReducers } from 'redux-persist'
import storage from 'redux-persist/es/storage'
...import my reducers here

const config = {
  key: 'root',
  storage
}

const reducers = persistCombineReducers(config, {
  ... ( my reducers here)
})

const appReducer = (state, action) => {
  if (action.type === 'CLEAR_DATA') {
    state = undefined
  }

  return reducers(state, action)
}

export default appReducer

This works fine. I have a logout function that dispatches action with type 'CLEAR_DATA' and I see in redux logger that the action does indeed clear the state. However, if I then close the application completely and then reopen it, it opens and all the data is somehow rehydrated. I am unsure if I possibly just have redux persist set up incorrectly. My code to set up redux persist is below

index.js

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('MyApplicationName', () => App);

App.js

import React from 'react'
import configureStore from './js/store/configureStore'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/lib/integration/react'
import MyApp from './js/MyApp'

const { store, persistor } = configureStore()

export default class App extends React.Component<{}> {
  render() {
    return (
      <Provider store={store}>
        <PersistGate persistor={persistor}>
          <MyApp />
        </PersistGate>
      </Provider>
    )
  }
}

configureStore.js

import { applyMiddleware, createStore, compose } from 'redux'
import { persistStore, persistCombineReducers } from 'redux-persist'
import thunk from 'redux-thunk'
import appReducer from '../reducers/'

function configureStore() {
  let store = compose(applyMiddleware(thunk))(createStore)(appReducer)

  let persistor = persistStore(store)

  /* Uncomment to purge store
  ======================== */
  //persistor.purge()

  return {
    persistor,
    store
  }
}

export default configureStore

My package versions "react-native": "0.51.0", "react-redux": "^5.0.6", "redux": "^3.7.2", "redux-persist": "^5.4.0",

connercms avatar Jan 13 '18 20:01 connercms

Very similar setup for me as regards app, configureStore, and rootReducer - the only exception being that I have nested persistReducers.

Same problem, I clear the state on a logout action, see it cleared by the action, but the cleared state isn't persisted. Logging in as a different user or refreshing/re-opening the page rehydrates the pre-cleared state.

DJTB avatar Jan 15 '18 03:01 DJTB

@DJTB As a workaround, inside my action creator I dispatch an object with type 'LOG_OUT', then in every single reducer file I have a case for 'LOG_OUT' and set state: initialState. It seems to persist this just fine. I used the code I posted above before I updated redux persist versions and it worked fine - not sure what changed

connercms avatar Jan 15 '18 14:01 connercms

I had an inkling I might have to clear explicitly within each nested reducer, though that's kind of a pain even with a reducer decorator. Thanks for the info - I'll give it a shot.

DJTB avatar Jan 15 '18 21:01 DJTB

I've tried various combinations of clearing state, and I often still get back stale state / or new state isn't persisted.

I see that clearing from the top level removes the _persist info from the state inside my nested reducers, and that completely messes up redux-persist?

DJTB avatar Jan 16 '18 01:01 DJTB

@DJTB how are you clearing state? like with an action or via persistor.purge or something else?

rt2zz avatar Feb 09 '18 17:02 rt2zz

I have the same problem here. Does anybody solve an issue?

DavitVosk avatar Feb 09 '18 18:02 DavitVosk

@DavitVosk can you elaborate on what the issue exactly is?

rt2zz avatar Feb 09 '18 18:02 rt2zz

@rt2zz Thanks for your response. So when I press Logout button, I call 2 actions with respective types: USER_LOGOUT and RESET. Based on the first type I return state=undefined in appReducer (as mentioned in this issue raise description). The second type RESET I use to reset the state (I use redux-reset). The outcome: when Logout is pressed, the app navigates to the welcome screen of the app (as it should be), BUT when I reload the app it goes to the first page of the app, namely I could not somehow nullify the redux state when logged out.

DavitVosk avatar Feb 09 '18 19:02 DavitVosk

ah ok so I am fairly certain there is an issue with setting state to undefined. We should add a test for this, I am not sure what the behavior will be.

In general redux persist assumes state is an object, ie not undefined. you might try something like setting state to an empty object on logout instead of undefined.

rt2zz avatar Feb 09 '18 19:02 rt2zz

@rt2zz I tried with setting empty object to state when action type is USER_LOGOUT, but unfortunately no success.

DavitVosk avatar Feb 09 '18 19:02 DavitVosk

The eventual solution I settled on (less than ideal) was to remove top level persistence and use nested persistors only, which seems to work for my case.

const rootReducer = combineReducers({
  notPersisted: someReducer,
  nestedPersisted: persistReducer(specificNestedPersistConfig, nestedReducer),
  /* more nested / unpersisted reducers */
});

Each nested persisted reducer shares a basic persist config ({ storage, version, debounce }), but merges its own specific key and whitelist.

The nested reducers all have their own initialState declared within the file. And they all have a final switch case for USER_LOGOUT : (state) => initialState

Sometimes initialState has keys/values, sometimes it's just {} but never undefined.

Localforage indexedDB ends up with: image

I'd prefer a single USER_LOGOUT at the top level (and a single DB entry / key), but I could never get it to work properly since it seemed to mess with the _persist entries in redux state which I think is what was causing the problems. @rt2zz I was clearing state at the top level exactly the same as the OP before changing to nested with their own initialStates.

DJTB avatar Feb 09 '18 22:02 DJTB

@DJTB ah thanks for clarity. So yes it probably does confuse the persist if you blow out state from the top. I would like to allow this however.

One probable solution is to store _persist state outside of the reducer. This seemingly opens up a few more use cases. There is an open issue for this somewhere that I cannot find atm. Not without challenges though, we would need to be very careful about out of sync state issues and possible performance impact.

Not sure what the timeline for such a change might be.

rt2zz avatar Feb 09 '18 22:02 rt2zz

@DJTB yes I could not though find a way to handle logout at the top level and without making each reducer to listening to my "USER_LOGOUT" action type. I also tried your above-mentioned structure with nestedPersisted reducers, but it was not working from my side. I just make sure all reducers listen to my USER_LOGOUT action type and nullify themself to their initial state. It works like a charm. @DJTB and @rt2zz Guys thanks for being ready to discuss and help :)

DavitVosk avatar Feb 10 '18 07:02 DavitVosk

But I think this issue should not be closed since the bug is still there. We just found an alternative solution.

DavitVosk avatar Feb 10 '18 07:02 DavitVosk

I have this issues as below: In Android, I listening event AppState for updating data.

componentWillUnmount(){
    AppState.removeEventListener('change', this.handleAppStateChange.bind(this));
}
handleAppStateChange (nextAppState) {
    if(nextAppState == 'background') {
      session.logout().then(() => {});
    }
}

session.logout(): it will remove data in store I have 2 case:

  1. I press home and close app (kill) => When I reopen app, data don't remove and store will restore old data.
  2. I press home and not close app (background) => When I reopen app, data will remove in store. => when you update data in store and close app => it don't work.

navata avatar May 11 '18 04:05 navata

Just ran into this today. Is there any other solution other than each reducer listening to the log out action?

anguyen1817 avatar Sep 13 '18 19:09 anguyen1817

I also have "CLEAR_APP" action type at every reducer. This action clears everything needed manually, all other solutions didn't not worked as expected.

artshevtsov avatar May 28 '19 10:05 artshevtsov

You can listen on actions at the root reducer, instead of checking for a log out at every reducer you have. However, I still have the problem of the state rehydrating when the application closes without invoking log out action.

bengansukh avatar May 29 '19 15:05 bengansukh

I'm so lost. I cannot seem to figure out to fit in the custom rootReducer (Dan's stack example). I even tried to mimic the example you gave directly at it breaks my app:

  const reducers = persistCombineReducers(persistConfig, myReducers)
  const appReducer = (state, action) => {
    if (action.type === 'USER_LOGOUT') {
      storage.removeItem('persist:root')
      state = undefined
    }
    return reducers
  }
  const store = compose(applyMiddleware(...middleware))(createStore)(appReducer)
  console.log(store.getState())
  const persistor = persistStore(store)
  return { store, persistor }

The store returns as a function and not an object. I've tried multiple ways with no luck. For instance this seemed the most logical to me:

  const rootReducer = combineReducers(myReducers)
  const persistedReducer = persistReducer(persistConfig, rootReducer)
  const appReducer = (state, action) => {
    if (action.type === 'USER_LOGOUT') {
      storage.removeItem('persist:root')
      state = undefined
    }
    return persistedReducer
  }

  const store = createStore(appReducer, enhancer)
  const persistor = persistStore(store)
  return { store, persistor }

But that also returns a function and not the store. But if I run

const store = createStore(persistedReducer, enhancer)

instead of

const store = createStore(appReducer, enhancer)

It returns the store fine, but the custom reducer needed to reset the store is left out.

Also I've seen no one mention the use of storage.removeItem('persist:root') which Dan Ab. specified in his stack if you're using redux-persist

orpheus avatar Aug 14 '19 16:08 orpheus

@navata @anguyen1817 @acheronte did you found a fix for the AppState bug?

componentWillUnmount(){
    AppState.removeEventListener('change', this.handleAppStateChange.bind(this));
}
handleAppStateChange (nextAppState) {
    if(nextAppState == 'background') {
      session.logout().then(() => {});
    }
}

gusgard avatar Aug 14 '19 19:08 gusgard

If you have a rootPersistConf object, then you can add a blacklist array that takes strings of your modules to prevent certain modules from being persisted. the modules revert back to the initial state.

mgarnick2000 avatar Sep 05 '19 15:09 mgarnick2000

@mgarnick2000 This is not exactly about blacklisting.. The idea is to be able to reset whole store to initial value on user logout without having case USER_LOGGED_OUT: return initialState in every reducer. It's not about not storing some parts of the reducer hierarchy.

Dema avatar Sep 05 '19 20:09 Dema

I faced the simlar problem. If i use undefined to reset the root state, new state will never be persisted. I find that the reason is here

JackClown avatar Sep 30 '19 07:09 JackClown

I am facing the same issue

BoKKeR avatar Dec 10 '19 20:12 BoKKeR

Same issue here. Setting state = undefined from root reducer stops data from being persisted until the app is restarted.

gauravahuja-unthinkable avatar Jan 13 '20 12:01 gauravahuja-unthinkable

I've found a working semi-hacky solution for my setup with nested persistance configs and a top-level RESET_STORE action. As it seems that several different problems are discussed in this thread, and because I don't want to make this post too long or diluted, I will just document my specific working solution instead of trying to apeal to certain problems discussed here.

I initialize my store with a rootReducer such as in "Dan's stack example", with a persistance config (I use localforage as storage):

const rootPersistConfig = {
  key: 'root',
  storage: localforage,
  blacklist: ['aReducer']
};

const store = createStore(
  persistReducer(rootPersistConfig, rootReducer),
  composeEnhancers(...)
);

The root reducer accepts actions just as in "Dan's stack example".

export function rootReducer(state, action) {
  if (action.type === RESET_STORE) {
    state = undefined;
  }
  return appReducer(state, action);
}

This setup allows me to call RESET_STORE, and have all the reducers reset to their initial state (by common reducer practice), without tampering with the root persistance object which controls persistance for the whole app.

However, all nested persistance configurations in appReducer will suffer from the RESET_STORE action by having their persistance object nullified. This stops their data from being persisted until the next refresh. A nested persistance configuration could look like this:

const aReducerNestedPersistanceConfig = {
  key: 'aReducer',
  storage: localforage,
  whitelist: ['keepOnlyThisValuePersisted']
};

const appReducer = combineReducers({
  aReducer: persistReducer(aReducerNestedPersistanceConfig, aReducer)
  aSecondReducer,
  aThirdReducer
});

This leads to my solution for keeping the persistance on nested persistance configs.. By adding a RESET_STORE case to just the reducers with nested persistance configs, it is possible to keep the persistance object from being nullified by copying it and just placing it in the store.

// Inside aReducer switch-case
case RESET_STORE:
  return {
    ...state,
    _persist: {
      version: -1,
      rehydrated: true
    }
  };
  ...

PS: I have not had any use for localforage.removeItem('persist:root') because the localforage storage updates (to a pretty much empty store) when the store is reset.

johannsl avatar May 12 '20 09:05 johannsl

The eventual solution I settled on (less than ideal) was to remove top level persistence and use nested persistors only, which seems to work for my case.

const rootReducer = combineReducers({
  notPersisted: someReducer,
  nestedPersisted: persistReducer(specificNestedPersistConfig, nestedReducer),
  /* more nested / unpersisted reducers */
});

Each nested persisted reducer shares a basic persist config ({ storage, version, debounce }), but merges its own specific key and whitelist.

The nested reducers all have their own initialState declared within the file. And they all have a final switch case for USER_LOGOUT : (state) => initialState

Sometimes initialState has keys/values, sometimes it's just {} but never undefined.

Localforage indexedDB ends up with: image

I'd prefer a single USER_LOGOUT at the top level (and a single DB entry / key), but I could never get it to work properly since it seemed to mess with the _persist entries in redux state which I think is what was causing the problems. @rt2zz I was clearing state at the top level exactly the same as the OP before changing to nested with their own initialStates.

This solution works for me Thanks

lorenz068 avatar Sep 11 '20 15:09 lorenz068

this is what I've do `const rootReducer = (state: any, action: any) => { if (action.type === 'logout') { purgeStoredState(config); return reducers(undefined, action); }

return reducers(state, action); };

export const STORE_GENERATOR = () => { const store = createStore( rootReducer, composeWithDevTools(applyMiddleware(...middlewares)), );

const persistor = persistStore(store); return {store, persistor}; }; `

trickyc0d3r avatar Oct 15 '21 15:10 trickyc0d3r

This bug is happening for me too and it's shame to see it not fixed after more than 4 years since this issue has been opened!

I'm using persistor.purge() upon logout like this:

  // Show logout message and purge the store
  useEffect(() => {
    if (isLoggedOut) {
      Toast.show({
        type: 'success',
        text1: logoutMessage,
      });

      persistor.purge();
    }
  }, [isLoggedOut]);

Everything works correctly but after closing and reopening the app, the previous state get rehydrated with the persist/REHYDRATE action and then upon seeing that isLoggedOut is true, persistor.purge() gets called again:

Screen Shot 2022-08-31 at 11 31 39 AM

n-ii-ma avatar Aug 31 '22 07:08 n-ii-ma

A few of you have correctly (I think) pointed out that clearing out the state from the top level also clears the _persist field in the nested reducer, which seems to be causing the issue. My solution is to simply run persistor.purge right before my top level action that clears the store (or resets it to its initial state).

persistor.purge();
dispatch({ type: LOGOUT });

I could argue that this is still a workaround and not a solution to the problem, but it seems totally adequate to me.

jackkrone avatar Jun 26 '23 20:06 jackkrone