rt2zz / redux-persist

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

Always loading initialState #189

Open eneskaya opened 7 years ago

eneskaya commented 7 years ago

Great package! When I tried this, it seems that it always loads the initial state for me. My code looks like this:

import { AsyncStorage } from 'react-native'
import { applyMiddleware, createStore, compose } from 'redux'
import { persistStore, autoRehydrate } from 'redux-persist'
import createLogger from 'redux-logger'

import reducersCombined from './reducers/combined'
import initialState from './initialState'

const middlewares = [
  createLogger()
]

const enhancers = [
  applyMiddleware(...middlewares),
  autoRehydrate()
]

const store = createStore(reducersCombined, initialState, compose(...enhancers))
persistStore(store, { storage: AsyncStorage })

export default store

What am I doing wrong here? Could be that I misunderstood something as I'm quite new to redux.

AymericG commented 7 years ago

I have the same experience. Trying to figure out what is wrong.

eneskaya commented 7 years ago

What I found out so far is, that it partially works when I pass undefined as the initial state, when calling createStore

const store = createStore(reducersCombined, undefined, compose(...enhancers))
persistStore(store, { storage: AsyncStorage })

My initial state looks something like the following:

const initialState = {
    user: {
        logged_in: false
    },
    questions: [
        { question_title: 'First question title', another_attribute: '...' },
        { question_title: 'Second question title', another_attribute: '...' }
    ]
}

What happens here is, that when I change the primitive value logged_in it gets persisted. Only the array questions is not being persisted. Is this the intended behaviour?

AymericG commented 7 years ago

When passing undefined as initial state, I can see the REHYDRATE event being raised, and I can see the payload looking correct, but the next state is totally wrong (missing stuff like with @eneskaya).

If I don't pass undefined as initial state, the REHYDRATE event is raised but nothing is done to the state, although the payload is correct.

eneskaya commented 7 years ago

@AymericG where/how do you see the REHYDRATE event being raised?

AymericG commented 7 years ago

@eneskaya In the console, I use redux-logger.

rt2zz commented 7 years ago

Hm, this sounds like a bug in autoRehydrate. Can you try adding autoRehdyrate({log: true}) and then report the log output here? Also what version of redux-persist are you on?

eneskaya commented 7 years ago

@rt2zz I'm on 4.0.0-alpha6 and the output is as follows:

redux-persist/autoRehydrate: 1 actions were fired before rehydration completed. 
This can be a symptom of a race condition where the rehydrate action may 
overwrite the previously affected state. 
Consider running these actions after rehydration:

redux-persist/autoRehydrate: sub state for key `questions` is falsy but initial state is an object, skipping autoRehydrate.

What does sub state for key 'questions' is falsy mean?

Edit:

"react": "15.3.2",
"react-native": "0.33.0",
rt2zz commented 7 years ago

that message logs when https://github.com/rt2zz/redux-persist/blob/master/src/autoRehydrate.js#L55 hits, meaning the inbound state is either null undefined or false. Is questions the only reducer which is failing to rehydrate?

ainesophaur commented 7 years ago

I'm noticing the same behavior. Would you like me to provide the logs?

Edit: Now that I think of it, I believe this would be the expected behavior when using multiple reducers and combineReducers. Each of my reducers has an initialState and the rootReducer also has an initialState. Previously, I would also set the initialState when creating the store, which would cause rehydrate to not update nested states/objects.

However, by setting createStore(rootReducer, initialState, compose()) to createStore(rootReducer, undefined, compose()) , redux-persist is now able to rehydrate the state properly and I still have an initialState for first-time run.

rt2zz commented 7 years ago

interesting. in the former case, how did you construct initialState? did the shape of that value look exactly like the shape of the resulting reducer tree?

ainesophaur commented 7 years ago

@rt2zz I used default params to construct it

Previously I had

configureStore(initialState = {}) {
  const store = createStore(
    rootReducer,
    initialState,
    compose(
      autoRehydrate(),
      applyMiddleware(
          thunk,
          ...middleware,
          createActionBuffer(REHYDRATE)
      )
    )
  );

I have an isomorphic app and I was providing an actual initialState to configureStore when the app is mounted client side (after SSR).. Im not sure why I thought I had to provide a default empty object initialState when calling configureStore without an actual state to restore.

Roilan commented 7 years ago

Same issue here with SSR. Doesn't seem to dispatch the initial state coming from the server. Also, I'm whitelisting one reducer.

mbifulco commented 7 years ago

I believe I'm running into this, too. Currently testing on react-native using an ios app.

Packages are as follows:

    "react": "15.4.1",
    "react-native": "0.39.0",
    "react-redux": "4.4.5",
    "redux": "3.5.2",
mbifulco commented 7 years ago

I'm also unable to get any logs to print, despite having used

autoRehydrate({log:true}) in the compose function for the middleware I'm using (which consists of this package and thunk.

ainesophaur commented 7 years ago

TL;DR: provide undefined as the second param to createStore.

I've only noticed the behavior when I provide an initial state as the second param to createStore. If you pass undefined or null then it rehydrates correctly. I haven't reviewed the code in depth, but I'd imagine it doesn't restore state objects where a value already exists in the store state during creation.

When you have multiple reducers and you provide an initial state to them, then your first run state will shape correctly. Then subsequent launches will restore the state from redux-persist

mbifulco commented 7 years ago

Strangely enough, that's what I'm doing. Here's a simplified snippet:

import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import thunk from 'redux-thunk';
import {AsyncStorage} from 'react-native'
import {persistStore, autoRehydrate} from 'redux-persist'

import { app } from './modules'
import { login} from './modules'

const middleware = compose(
  applyMiddleware(thunk),
  autoRehydrate({log:true}),
);

export default (data = {}) => {
  const rootReducer = combineReducers({
    //every module's reducer defined here
    [app.NAME]: app.reducer,
    [login.NAME]: login.reducer
  })

  return createStore(rootReducer, undefined, middleware)

  persistStore(store, {storage: AsyncStorage})
}
mbifulco commented 7 years ago

Shoot. persistStore happens after return? Good idea, self.

JulianKingman commented 7 years ago

So if i create my reducers like this:

const reducers = (state  = {}, action) => {
  switch (action.type) {
    case 'ADDED':
    ....
  }
}

Does that state = {} default declaration count as initializing the state? I'm wondering if that's why my store is not rehydrating.

ainesophaur commented 7 years ago

No that's fine.. That's how I do it. You just cannot pass a default initial state to the createStore function On Jan 5, 2017 8:52 AM, "JulianKingman" notifications@github.com wrote:

So if i create my reducers like this:

const reducers = (state = {}, action) => { switch (action.type) { case 'ADDED': .... } }

Does that state = {} default declaration count as initializing the state? I'm wondering if that's why my store is not rehydrating.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/rt2zz/redux-persist/issues/189#issuecomment-270648388, or mute the thread https://github.com/notifications/unsubscribe-auth/AEeBdpqdSzs0WM7y2E0pCmQlwsm4f2XFks5rPPWEgaJpZM4KR1gi .

DonovanCharpin commented 7 years ago

@JulianKingman same as you. Everything looks fine, I just see that the autoRehydrate is skipped because an action is triggered. I don't know where because even the redux-logger doesn't say anything before this log from autoRehydrate..

const app = combineReducers({
 ...
  carts
});

const enhancers = compose(
    applyMiddleware(
        thunk, 
        loadedSoundsMiddleware, 
        redux-logger, 
        createActionBuffer("persist/REHYDRATE")
    ),
    autoRehydrate({log: true})
);

// Create and persist the store to the local storage
const store = createStore(app, undefined, enhancers);

// We store only the checkout process with cart etc.
const persistConfig = {
  whitelist : ["carts"]
};
persistStore(store, persistConfig);

export default store;

And my carts store looks like that :

const initialState = {
  all: {}
};

const planReducer = (state, action) => {
  if (typeof state === 'undefined') {
    return initialState;
  }
  switch (action.type) {
    case ...:
     ...
  }
};

export default planReducer

This is my log from the autoRehydrate

redux-persist/autoRehydrate: 1 actions were fired before rehydration completed. This can be a symptom of a race condition where the rehydrate action may overwrite the previously affected state. Consider running these actions after rehydration:

redux-persist/autoRehydrate: sub state for key carts modified, skipping autoRehydrate.

JulianKingman commented 7 years ago

I ended up manually implementing the reducer, and now it works fine, more info here: https://github.com/rt2zz/redux-persist/issues/244#issuecomment-270655356

DonovanCharpin commented 7 years ago

Thanks @JulianKingman, I fixed on my side adding this piece of code in my reducer. It's extracted from the READ_ME, that's not great to add a case in the switch but it works :

import {REHYDRATE} from 'redux-persist/constants'

case REHYDRATE:
      var incoming = action.payload.carts; // Carts is the name of the reducer
      if (incoming) return {...state, ...incoming};
      return state;
harrisrobin commented 7 years ago

There is 2 things I did to solve this without having to resort to an opt-in per reducer rehydrate.

  1. ~Initially, in my store i was exporting a create function rather than the store and creating the store in my entry index.js. Simply exporting my store instead of a function worked.~
  2. Added undefined as the initial state in my reducer.

Keep in mind that doing 2 alone did not work, I had to do 1 as well.

EDIT: only point 2 is necessary

andrefox333 commented 7 years ago

@HarrisRobin can you show a code snippet of what you mentioned above?

TheoMer commented 7 years ago

@HarrisRobin I too would be interested in seeing any code snippets you can share.

bhavyanth7777 commented 7 years ago

I was able to correct this without having to use undefined as the initial state. Here's the code

In index.android.js

export default class AppName extends Component {
  constructor(){
    super();
    this.state = {rehydrated: false}
  }
  componentWillMount(){
      persistStore(store,{storage: AsyncStorage},()=> {
        console.log("rehydrated");
        this.setState({rehydrated:true});
      });
  }
  render() {
    if(this.state.rehydrated){
      console.log(store.getState());
        return (
            <App store={store}/>
        )
    }
    else {
      return(
          <View>
            <Text style={{marginTop:100, alignSelf:'center'}}>Setting up...</Text>
          </View>
          )
    }
  }
}

AppRegistry.registerComponent('AppName', () => AppName);

I'm making sure that the rehydration is complete in componentWillMount() and then passing the store to the provider as a prop. While it rehydrates, I'm displaying a text "Setting up.." on the screen.

Store configuration

function configureStore(initialState) {
    const enhancer = compose(
        applyMiddleware(
            thunkMiddleWare,
            loggerMiddleWare
        ),
        autoRehydrate()
    );
    return createStore(reducer, initialState, enhancer);
}
Nualiian commented 7 years ago

I'll share my experience here:

A combination of @bhavyanth7777 and @JulianKingman solution worked for me. I had to make use of the callback after rehydration to load my main component, but also implement the persist/REHYDRATE in my reducer manually. It looks kinda hacky, but it works and, most importantly, I can still make use of initialState, which is cool.

dmexs commented 6 years ago

Struggled with this issue tonight. Finally figured out I had an error in my reducer. In my reducer I was writing:

Object.assign(state, {new_value: 'blah blah'})

as oppososed to:

Object.assign({}, state, {new_value: 'blah blah'})

See this SO post for more detail: https://stackoverflow.com/questions/33828267/why-do-redux-examples-pass-empty-object-as-first-object-assign-argument

leguma commented 6 years ago

I'm using v5 and am seeing the problem as well. I noticed this issue can occur if you have state mutations triggered on app load (before rehydration is complete).

Example:

export default (state = defaultState, action) => {
    // Standard switch handler
    let newState = handleAction(state, action)

        // This always runs *AFTER* the reducer's other state changes. E.g. some flag setting any time a redux event fires
    return someMutation(newState);
}

The above somehow causes the rehydrate to be aborted, despite having the same state shape (in my case) before & after the mutation. It's probably checking to see if the reference object has changed.

There are a few ways to fix it:

  1. Make your "global" mutation only fire when the input object is a different object from the one returned by handleAction.
export default (state = defaultState, action) => {
    let newState = handleAction(state, action)

    // handleAction default action handler returns false or null. Could check obj equivalence instead if you'd rather return the passed-in state as the default handler.
    if (!newState) {
        return state
    }

    return someMutation(newState);
}
  1. Don't make your mutation global. Either thunk all your desired action creators with this "global" action, or wrap the mutation on each handler you want it on. This could also make it a bit more performant (at the cost of being uglier).

    export default (state = defaultState, action) => {
    switch (action.type) {
        case 'ACTION_A': return someMutation(
            // Your standard handler for this action
        )
        case 'ACTION_B': return someMutation(
            // Your standard handler for this action
        )
        case default:
            // If you touch put a mutation here, it will break rehydration.
            return state
    }
  2. As mentioned by others in the thread, add a rehydrate handler that purposefully doesn't touch state:

    
    import { REHYDRATE } from 'redux-persist'

export default (state = defaultState, action) => { // Workaround to prevent interruption of rehydration if (action.type === REHYDRATE) { return state }

return someMutation(handleAction(state, action));

}



All of these options feel a bit hacky. Maybe we could tell redux-persist to force rehydrate regardless of the incoming state?

EDIT: Note an alternative to the above is by creating middleware, which is perhaps a better cross-cutting approach to this sort of behavior. I use the above as examples of where rehydration can be aborted.
rt2zz commented 6 years ago

@leguma I am open to having redux-persist always rehydrate, or at least exposing this behavior somehow. It is definitely a source of confusion. I hate to add more config options though, so perhaps this should be a separate stateReconciler?

FWIW we do warn when this happens while debug: true...

It is unfortunate because the underlying complexity around this is actually quite low, it is just an issue with communication / api ergonomics. Even persistReducer vs persistCombineReducers I think leads to confusion. We need a new naming scheme.

galcivar commented 6 years ago

I have been struggling with this for weeks, see: https://stackoverflow.com/questions/47971918/using-connect-with-react-redux-and-redux-persist Tried @leguma 3 ways without avail. Please anyone can help me on this?

Baka9k commented 6 years ago

I had to pass config from server to app in initial state (I use SSR), but redux-persist worked only if i pass {} in initial state in configure-store.js. I fixed this by blacklisting state field I needed to pass from server with initial state, and passing state only with this field as initial state:

function configureStore(isHotLoaderRequired = false) {
    return (initState = {}, history = null) => {
        const store = createStore(
            persistedReducer,
            { settings: initState.settings }, // <- here! If I pass here initState, redux-persist not works
            applyMiddleware(
                routerMiddleware(history),
                thunk
            )
        );
. . .
}
yasserzubair commented 6 years ago

this is how i create my store

const persistConfig = {
 key: 'root',
 storage: storage,
 stateReconciler: autoMergeLevel2 // see "Merge Process" section for details.
};

const pReducer = persistReducer(persistConfig, rootReducer);

export function configureStore(history) {
  const middleWares = [thunk];
    middleWares.push(createLogger());
  const middleware = applyMiddleware(...middleWares);

  return createStore(pReducer, middleware);
}
export  const persistor = persistStore(configureStore());

This is how I initialize my app

import { PersistGate } from 'redux-persist/lib/integration/react';
import { persistor, configureStore } from './store';

<Provider store={configureStore()}>
  <PersistGate loading={<Text>Loading</Text>} persistor={persistor}>
    <Root>
      <AppNavigator />
    </Root>
  </PersistGate>
</Provider>

Checked by passing undefined as the second argument. Still no luck. Please tell me what I'm doing wrong.

Johncy1997 commented 6 years ago

I created my store like this..

configureStore.js:


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

 import allReducers from '../reducers';

 const middleware = [
   thunk,
     logger
   ];
  const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

   export default function configureStore(){
   let store = createStore(
       allReducers,
         {},
        composeEnhancers(applyMiddleware(...middleware))
 );
    console.log("Store created here"+store);
    return store;
  }

reducers/index.js:


 import {persistCombineReducers} from 'redux-persist';
  import storage from 'redux-persist/es/storage';
  import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
     import autoMergeLevel from 'redux-persist/es/stateReconciler/autoMergeLevel1';

  import Authentication from './Authentication';
  import Homeworks from './Homeworks';
      import ClassConfig from './ClassConfig';

  const config = {
  key :'root',
  storage,
  stateReconciler: autoMergeLevel2,
 // stateReconciler: autoMergeLevel,
  whitelist:['authentication'],
 blacklist:['homeworks','classconfig']
}

  const allReducers =  persistCombineReducers(config,{
   authentication: Authentication,
   homeworks: Homeworks,
   classconfig: ClassConfig
});

     export default allReducers;

Authentication.js:


  import { LOGIN, LOGOUT, GET_USER_DETAILS,REQUEST_FAIL } from '../actions/actionTypes';
  // import { REHYDRATE } from 'redux-persist';

 const DEFAULT_AUTHENTICATION = {
     email: null,
   name: null,
  token: null,
    signedIn: false,
   error: null,
   userDetail:{},
     logoutmessage:null
   }

  const Authentication = (state = DEFAULT_AUTHENTICATION, action) =>{
    switch(action.type){
      case LOGIN:
        console.log("reducers/Authentication/LOGIN called"+state);debugger
        return{
            ...state,
            token: action.response.success.token,
            name: action.response.success.name,
            signedIn: true
        }
    case LOGOUT: 
        console.log("reducers/Authentication/LOGOUT called");
        return{
            ...state,
            logoutmessage:action.response.success,
            token : null,
            name : null,
            signedIn : false
        }
    case GET_USER_DETAILS:
        console.log("reducers/Authentication/GET_USER_DETAILS called");
        return{
            ...state,
            userDetail: action.response.success,
            email: action.response.success.email,
            name : action.response.success.name
        }
    case REQUEST_FAIL:
        console.log("reducers/Authentication/REQUEST_FAIL called");
        return{
            ...state,
            error: action.error,
        }
     case "persist/REHYDRATE": 
             const data = action.payload;
            if (data) 
                return {
                    ...state,
                     ...data.authentication
                    } //Should I compulsorily write this case?If i dont write it,the payload is not replacing the next state while rehydrating the store(i.e store gets only initial state).Here my doubt is for each whitelist state should i include the line as ...data.whitelist1 , ...data.whitelist2, ...data.whitelist3...?

     default:
        console.log("default/reducers/Authentication called");
        return {
            ...state
        }
        }
   }

      export default Authentication;

Login.js:


 //import stmts

     const mapStateToProps = (state) =>{
    return{
      authentication :state.authentication,
     }
      }
      const mapDispatchToProps = (dispatch) => {
       return{
          onSubmit : (credentials) => {
      dispatch(login(credentials));
         }
       }
          }

  class Login extends Component {
    constructor(props){
      super(props);
        this.state = {
       email:null,
        password:null,
         otp:null
    }
  }
componentDidMount(){
Toast.show('Welcome to Edfish', Toast.LONG);
}

 submit(callback){
    let credentials={};
   credentials.email=this.state.email,
    credentials.password=this.state.password,
    this.props.onSubmit(credentials);
    callback();
    }
 render() {
  return (
     <Container >
      <StatusBar backgroundColor={styles.mystatusbar} barStyle='dark-content'/>
        <Content contentContainerStyle={{ justifyContent: 'center', flex: 1,marginLeft:15,marginRight:15 
          }}>
              <Text style={{alignSelf:"center"}}>EDFISH</Text>
              <Label style={{paddingTop:25,paddingBottom:15,fontWeight:'800'}}>Enter email id:</Label>
              <View style={[styles.loginInput,styles.h_50]}>
                <Input  placeholder="myemail@gmail.com" onChangeText={(email)=>{
                  this.setState({
                    email:email
                  });
                }} 
          onSubmitEditing={()=>{
                  this.refs.password._root.focus();
                }}
           blurOnSubmit={false}/>
              </View>
              <Label style={[styles.pad_top_25,styles.pad_bot_15,styles.font_w8_800]}>Enter your password: 
             </Label>
              <View style={[styles.loginInput,styles.h_50]}>
                <Input  placeholder="your password" secureTextEntry={true} onChangeText={(password)=> 
                         {
                  this.setState({
                    password:password
                  });
                }} 
                 onSubmitEditing={(event)=>{
                  this.submit(function() {
                    console.log('huzzah, I\'m done!');
                });//The if stmt gets checked before my reducers/LOGIN gets completed the changes in the store
                  if(this.props.authentication.error !== null){
                    this.props.navigation.navigate('Home')
                  }
                  else{
                    Toast.show('Incorrect username or password');
                  }
                }}  ref="password"/>
              </View>
              <View style={{paddingTop:25,paddingBottom:15,}}>
              <Button  block onPress={ ()=>{
                this.submit(function() {
                  console.log('huzzah, I\'m done!');
              });debugger
                if(this.props.authentication.error !== null){
                  this.props.navigation.navigate('Home')
                }
                else{
                  Toast.show('Incorrect username or password');
                }
              }} style={{
                height:50,
                borderRadius:5,
                backgroundColor:'#3883c0'
                }}>
                <Text style={{alignSelf:'center',
                color:'white',
                fontSize:18,
                fontWeight:'500',
                paddingTop:10,
                paddingBottom:10}}>Login</Text>
              </Button>
              </View>

        </Content>
     </Container> 
   );
     }
    }

       export default connect(mapStateToProps,mapDispatchToProps)(Login); 

App.js/render method:


  const store = configureStore();
     store.subscribe(() =>
      console.log('Store State: ', store.getState())
      );
       const persistor = persistStore(
          store,
          {},
      ()=>{
             console.log('rehydration completed!!!!', store.getState());
    }
  );//I have checked by passing undefined also!
 render() {
   return (
   <Provider store={store}>
    <PersistGate loading={null} persistor={persistor} >
      <MyRoot/>
    </PersistGate>
  </Provider>
    );
  }

Now here my all confusion is that .. when i close the app and simple opens store state is persisted.If i reload the app store gets initial state.I am struggling to figure out the problem.Please someone tell me the solution for the comments i mentioned in the code.

Thanks in advance.

UmaMoiseenko commented 6 years ago

return createStore(pReducer, middleware);

@yasserzubair second argument in this call should be an initial state (you can also pass undefined or empty object) return createStore(pReducer, undefined, middleware);

I hope this works :)

gogoku commented 5 years ago

Hi,

Iam using redux-offline,

I am facing same issue as @Johncy1997

Below is how i am creating my store

let customConfig = {
    ...offlineConfig,
    persistOptions:{
        key: 'root',
        transforms: [immutableTransform()],
    },
    returnPromises : false,
    persistCallback : () => {this.setState({rehydrated : true})},
};

const { middleware, enhanceReducer, enhanceStore } = createOffline(customConfig);
let middlewares = applyMiddleware(offlineCommitHandler,thunk,middleware,);
store = createStore(enhanceReducer(IndexReducer),undefined,compose(enhanceStore,middlewares,persistAutoRehydrate({log:true})));

I have multiple reducers The issue occurs only in one reducer,

I placed a debugger in autoRehydrate, when opening the app first time it merges the data for that reducer, When opening the app second time inbound state for that reducer is null.

DonovanCharpin commented 5 years ago

Hi @gogoku, already had this issue when I had a mutation in one of my reducer. You could plug a mutation detector in redux middleware to be sure nothing mutate.

gogoku commented 5 years ago

Hi @DonovanCharpin , I checked for mutations using the library redux-immutable-state-invariant. Also i manually wen through the code for the reducer and i couldn't find any mutations occurring.

gogoku commented 5 years ago

Thanks @DonovanCharpin

Actually my issue was that the data was hitting the storage limit for asyncStorage in android and the data was being deleted.

Details can be found at this issue #199

Solved this using file system storage instead of asyncStorage https://github.com/robwalkerco/redux-persist-filesystem-storage

OlegPanfyorov commented 5 years ago

Same for me, any help here?

leguma commented 5 years ago

Check out my earlier comments regarding changing state during REHYDRATE events. If you've determined that you're definitely not doing that, then check to see if @gogoku's issue is related.

For me, my issue was that I had a default reducer handler that was changing some state (setting an expiresAt property and some other things). The REHYDRATE event, therefore, was causing a state mutation. In this case, redux-persist aborts (or perhaps there's a race condition), resulting in the rehydrated state being lost.

My solution was simply to prevent any state changes during the REHYDRATE event:

export default (state = defaultState, action) => {
    // do not mutate state during REHYDRATE!
    if (action.type === REHYDRATE) {
        return state
    }

    // handleAction is where the guts of this reducer lives. 
    // wrapperMutation happens AFTER the reducer logic; it _always_ mutates state (in this case, updating my expiresAt property). 
    // This would break rehydration if not for the above if statement.
    return wrapperMutation(handleAction(state, action));
}
OlegPanfyorov commented 5 years ago

@leguma Thanks, yep the problem was in my reducer. My mistake was in "default: return { ... state }" not "return state"

pwnreza commented 5 years ago

@OlegPanfyorov worked for me as well. Does someone know why this solution works?

UlyssesInvictus commented 5 years ago

If anyone wants to know why the original issue in this thread was happening (specifically: any instance of initialState being passed manually to createStore causes rehydration to not work), then it's pretty simple: redux always calls an @@INIT action when you pass this argument in (this is correct, and is documented by redux in the API for createStore).

redux-persist then notices that the store was updated, and persists your initial state to storage.

It then tries to hydrate your initial store with the state you literally just persisted, which is obviously a no-op.

I'm not totally sure that this is what's happening, but I'm fairly confident.

I personally haven't found a fix yet that meets all my needs, but I can see a potential few:

UlyssesInvictus commented 5 years ago

Update: ~the solution I chose for myself was to add a transform for the states I cared about, which detected whether this was an initial Redux commit, and returned an empty persist object ({}) if so (well, technically I returned the fields I cared about with the interfering fields stripped out, but your use case would likely be simpler).~

~This wasn't ideal, because I have to make such a transformer for each state and condition I care about, but that's also not terrible because you're essentially self-documenting every such case.~

This turned out not to work, because redux-persist doesn't have a method (as far as I can tell) for resolving merge conflicts in the inbound route to storage (in the same way that it does for outbound data from storage to Redux), so any act of persistence is all-or-nothing. I can see some easy fixes where either that method is added to the redux-persist API, the whitelist and blacklist config settings become functions that can take the incoming data to decide whether to persist (rather than just using key names), etc.

Unfortunately, I'm a little short on bandwidth; otherwise I'd implement all these myself and add a PR; so for now I did regrettably just have to refactor my app to avoid using preloadedState to the best of my ability (or, specifically, avoid using the fields that are triggering the persistedReducers that I have set up).

In the meantime, I think it would probably be a good idea to add to the documentation that redux-persist works poorly if you must make use of preloadedState in redux's createStore method.

pencilcheck commented 5 years ago

What is the workaround? I'm still confused. Sounds like the default usage is not working anymore. And I am also encountering this issue where the state is persisted when using the app but when reloading, the persisted state got overwritten by the initial state instead of loading them...

pencilcheck commented 5 years ago

My workaround is to make it as a singleton, don't have time to dive into why this works but it works for me.

import { combineReducers, createStore, compose, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'
import { persistStore } from 'redux-persist'

//import reducers from '../reducers'
import persistReducer from '../utils/persistReducer'

let store = createStore(
  persistReducer,
  {},
  composeWithDevTools()
);
let persistor = persistStore(store);

export default function create(initialState) {
  if (!store) {
    store = createStore(
      persistReducer,
      initialState,
      composeWithDevTools()
    )
  }
  return store
}

export function getPersistor(store) {
  if (!persistor) {
    persistor = persistStore(store)
  }
  return persistor
}
Moinax commented 5 years ago

@leguma Thanks, yep the problem was in my reducer. My mistake was in "default: return { ... state }" not "return state"

Yeaaah you made my day. That was actually my mistake too. We should never return a shallow copy of the state when we actually don't want to change the state at all !

aplattinum commented 4 years ago

I found that my reducer was being called multiple times before my the hydration of redux-persist. That way it reset state with the initial state then used that to hydrate the app.

It can happen if you're importing a function which carries the store and the persist variable.

It's better just to directly import the store and persist vars to make sure that they aren't being called a number of times.

rt-cff commented 3 years ago

Update: ~the solution I chose for myself was to add a transform for the states I cared about, which detected whether this was an initial Redux commit, and returned an empty persist object ({}) if so (well, technically I returned the fields I cared about with the interfering fields stripped out, but your use case would likely be simpler).~

~This wasn't ideal, because I have to make such a transformer for each state and condition I care about, but that's also not terrible because you're essentially self-documenting every such case.~

This turned out not to work, because redux-persist doesn't have a method (as far as I can tell) for resolving merge conflicts in the inbound route to storage (in the same way that it does for outbound data from storage to Redux), so any act of persistence is all-or-nothing. I can see some easy fixes where either that method is added to the redux-persist API, the whitelist and blacklist config settings become functions that can take the incoming data to decide whether to persist (rather than just using key names), etc.

Unfortunately, I'm a little short on bandwidth; otherwise I'd implement all these myself and add a PR; so for now I did regrettably just have to refactor my app to avoid using preloadedState to the best of my ability (or, specifically, avoid using the fields that are triggering the persistedReducers that I have set up).

In the meantime, I think it would probably be a good idea to add to the documentation that redux-persist works poorly if you must make use of preloadedState in redux's createStore method.

For the late comer, with the stateReconciler, I believe we can give up persisted state if it is conflicting with preloadedState. image