rt2zz / redux-persist

persist and rehydrate a redux store
MIT License
12.97k stars 867 forks source link

Store rehydrates when close app and reopen even after clearing data #659

Open connercms opened 6 years ago

connercms commented 6 years ago

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",

DJTB commented 6 years ago

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.

connercms commented 6 years ago

@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

DJTB commented 6 years ago

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 commented 6 years ago

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?

rt2zz commented 6 years ago

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

DavitVosk commented 6 years ago

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

rt2zz commented 6 years ago

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

DavitVosk commented 6 years ago

@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.

rt2zz commented 6 years ago

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.

DavitVosk commented 6 years ago

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

DJTB commented 6 years ago

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.

rt2zz commented 6 years ago

@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.

DavitVosk commented 6 years ago

@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 commented 6 years ago

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

navata commented 6 years ago

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.
anguyen1817 commented 6 years ago

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

artshevtsov commented 5 years ago

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.

bengansukh commented 5 years ago

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.

orpheus commented 5 years ago

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

gusgard commented 5 years ago

@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(() => {});
    }
}
mgarnick2000 commented 5 years ago

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.

Dema commented 5 years ago

@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.

JackClown commented 5 years ago

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

BoKKeR commented 4 years ago

I am facing the same issue

gauravahuja-unthinkable commented 4 years ago

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

johannsl commented 4 years ago

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.

lorenz068 commented 4 years ago

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

trickyc0d3r commented 3 years ago

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}; }; `

n-ii-ma commented 2 years ago

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
jackkrone commented 1 year ago

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.

SoldierSacha commented 5 months ago

I'm still having this issue.