Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rehydratedState is not recalculated on UPDATE_ACTION #120

Open
dimitriy-k opened this issue Apr 3, 2019 · 20 comments · May be fixed by #122
Open

rehydratedState is not recalculated on UPDATE_ACTION #120

dimitriy-k opened this issue Apr 3, 2019 · 20 comments · May be fixed by #122

Comments

@dimitriy-k
Copy link

dimitriy-k commented Apr 3, 2019

rehydratedState is defined only on INIT_ACTION. When localStorage is updated and then lazy-module is loaded, UPDATE_ACTION called and old state (rehydratedState from INIT) is used.

if ((action.type === INIT_ACTION || action.type === UPDATE_ACTION) && rehydratedState) { nextState = merge({}, nextState, rehydratedState); }

Use case:

  1. Login => token is saved in localstorage
  2. Reload the page => rehydratedState created with token
  3. Login with different user => new token stored in localstorage (but not synced with rehydratedState!)
  4. Go to lazy loaded page => UPDATE_ACTION, token is overridden with old token from step 2 :(((
@dimitriy-k
Copy link
Author

can you move https://github.com/btroncone/ngrx-store-localstorage/blob/master/src/index.ts#L233
to line 252, so rehydratedState always recalculated

or change the order to
nextState = merge({}, rehydratedState, nextState);
in that case some tests will fail

@BovineEnthusiast
Copy link

Having the exact same use case and issue.

@dimitriy-k
Copy link
Author

Hello, anyone?.... is that project dead?

@bufke
Copy link
Collaborator

bufke commented Apr 10, 2019

It's not dead but no one is actively working on it that I know of. btroncone is still doing releases when pinged. If you can make a Pull Request that clearly demonstrates the problem in a test and a fix that makes the test pass I could review it when I find some time.

@dimitriy-k
Copy link
Author

@bufke I have tried to create pull request, but I do not have permissions for this repo

@btroncone
Copy link
Owner

@bufke Is correct, we are not actively adding new features but we are trying to keep the project stable as best as possible. @dimitriy-k you shouldn't need permissions to create a pull request, only to merge PR's. Let me know when you have this available, thanks!

@dimitriy-k
Copy link
Author

@btroncone I am not able to push new branch, how can I create pull request?

@BovineEnthusiast
Copy link

@dimitriy-k Have you forked the project? I don't see it in your GitHub profile. Make sure you're not cloning this project but rather made a fork and cloned that.

@btroncone
Copy link
Owner

Hello @dimitriy-k , you would create a fork of this repo, make your updates, then create a pull request. Please see this explanation for additional details, thanks! https://help.github.com/en/articles/creating-a-pull-request-from-a-fork

@dimitriy-k
Copy link
Author

looks like UPDATE_ACTION is not needed at all

#122

@dimitriy-k
Copy link
Author

@bufke can you review PR?

@CleverCoder
Copy link

I spent almost all day trying to track down what's causing old state to appear out of nowhere. This should be considered a high priority security issue, if multiple people share a browser. It's possible for an affected application to clear out state, even have it synched to storage, only to have the original hydrated state be merged in.
Thank you!!

@CleverCoder
Copy link

Well, I discovered today that due to the comments mentioned in the original report (INIT not being called for lazy loaded module metareducers), I needed to re-add the check for UPDATE. A better solution just seems to be to clear out the captured value of hydratedState once it's used. That seems to fix the original issue, but also prevent any accidental re-applying of the restored hydrated state (which could live on, long after ngrx state has been cleared)

@bufke
Copy link
Collaborator

bufke commented Jul 29, 2019

This issue confuses me and I'm not that familiar with the original code base. @CleverCoder are you saying you have an alternative proposed fix different from #122

Is it easy to reproduce these issues? I'm guessing I never have been affected by this because I explicitly clear localstorage on logout. I also always handle server side auth errors by ensuring the user is fully logged out, including clearing localstorage.

@CleverCoder
Copy link

CleverCoder commented Jul 29, 2019

Yeah.. in my testing I found that the INIT_ACTION action wasn't being called for the lazy loaded modules. I wanted to understand the expected lifecycle, but couldn't find any NgRx reference that could help. What I did observe repeatedly was that for the lazy loaded modules, the first action being passed to the metareducer would be UPDATE_ACTION, and not INIT_ACTION. The current behavior of the code was to merge in the rehydrated state for either action. The problem I originally saw that led me here was that the rehydrated variable was not being cleared. I'm still not sure what triggered it, but the UPDATE action was disptched again, which caused the rehydrated state to be re-applied.

The fix I'm proposing is simply to leave the original code, where it applies the rehydrated state for either action, but once it's been applied clear out the rehydratedState variable to prevent it from accidentally being re-applied again. If we could rely 100% that the INIT action would be called for all module types, only once, then the fix proposed in #122 could work.

I'll try and put together a test case, but it's a pretty busy week.

@rowandh
Copy link
Contributor

rowandh commented Oct 10, 2019

If you want to skip all UPDATE_ACTION actions and don't want to wait for this PR to be merged, there's a simple way to do it. Replace your local storage sync reducer with a variant of this:

export function localStorageSyncReducer<T, V extends Action = Action> (reducer: ActionReducer<any>): ActionReducer<T, V> {
  
  return (state, action) => {
    if (action.type === '@ngrx/store/update-reducers')
      return reducer(state, action);

    return localStorageSync({
      rehydrate: true,
      removeOnUndefined: true,
      keys: []
    })(reducer)(state, action)
  };
}

@BBlackwo BBlackwo linked a pull request Apr 17, 2020 that will close this issue
@BBlackwo BBlackwo linked a pull request Apr 17, 2020 that will close this issue
@BBlackwo
Copy link
Collaborator

This issue seems pretty complicated, given all the different discussions going on.

Is someone able to provide a repro for this? You might be able to fork this stackblitz which was used to reproduce a different issue.

@sqrter
Copy link

sqrter commented Jul 7, 2021

We had the same use case causing the previous logged-in user to become active once the other user clicked on lazy-loaded route. Here's our meta reducer to sync session to/from local storage (old version).

export function sessionSyncReducer(reducer: ActionReducer<any>) {
  return localStorageSync({
    keys: [
      {user: ['session']}
    ],
    rehydrate: true,
    removeOnUndefined: true,
    syncCondition: (state: IAuthStore) => state.rememberMe === true
  })(reducer);
}

This version works like expected:

export function sessionSyncReducer(reducer: ActionReducer<any>) {
  return (state, action) => {
    return localStorageSync({
      keys: [
        {user: ['session']}
      ],
      rehydrate: true,
      removeOnUndefined: true,
      syncCondition: (state: IAuthStore) => state.rememberMe === true
    })(reducer)(state, action);
  }
}

@christoment
Copy link

christoment commented Apr 28, 2022

Having similar problem with my setup and digging through Google landed me in this issue.

#120 (comment) works as well for me, thanks!

I'm still wondering why wrapping localStorageSync in another function fixes the problem...

- C
ssw.com.au

@FrederikRobijns
Copy link

FrederikRobijns commented Jun 5, 2024

I just ran into this issue and it took me some time to find the exact source of the problem. What is going wrong is that the merge is potentially happening multiple times (with each INIT or UPDATE event) with an outdated rehydrated state.
To me this indicates two solutions:

  1. Ensure that the rehydration is only run once. This can ealsily be achived with something like a tracking flag.
  2. Ensure that the rehydration state is not outdated, by updating it whenever the state is synced to local storage.

I dont see any use case to support multiple rehydrations, so solution 1 is to me the most obvious.

I locally fixed this issue by using a custom merge reducer, something like this:

export const getSingleRunMergeReducer = (): ((state: any, rehydratedState: any, action: any) => unknown) => {
  let stateHasBeenHydrated = false;

  return (state, rehydratedState, action) => {
    if ((action.type === INIT_ACTION || action.type === UPDATE_ACTION) && rehydratedState && stateHasBeenHydrated === false) {
      state = defaultMergeReducer(state, rehydratedState, action);
      stateHasBeenHydrated = true;
    }
    return state;
  };
};

And then use that as mergeReducer whenever we create the localStorageSync:

  return localStorageSync({
    rehydrate: true,
    mergeReducer: getSingleRunMergeReducer()
  })(reducer);

Is this project still active? Can I create a PR with this solution?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants