btroncone / ngrx-store-localstorage

Simple syncing between @ngrx store and local storage
MIT License
608 stars 119 forks source link

Rehydrate causes unrelated keys not visible in the store #269

Open MikeDabrowski opened 1 week ago

MikeDabrowski commented 1 week ago

my sync config:

const localStorageSyncConfig: LocalStorageConfig = {
  keys: [{ appState: ['mode'] }],
  rehydrate: true,
  storageKeySerializer: (key: string) => `ngrx-sync-${key}`,
};

my store (relevant piece)

export const initialState: AppState = {
  mode: Mode.Light,
  countries: [],
  version: undefined,
  build: undefined,
  edition: undefined,
  test: 'undefined',
  localStorage: {
    restorePdfTooltipDismissed: true,
  },
};

And after the app starts. The store does not have test and localStorage properties at all. Even though they were not mentioned in the config. The properties that are in the store are set later (after rehydration) via various other actions. The missing ones I added while debugging, they are only in the initialState, not mentioned in other places.

I'd expect them to be ignored because they are not mentioned in the config. I am now afraid to use this tool because it can remove other properties too.

Actually it is a duplicate of: https://github.com/btroncone/ngrx-store-localstorage/issues/268

MikeDabrowski commented 1 week ago

I spent few hours to investigate

in ngrx the feature store reducer is defined more-less like so:

reducer(state = initialState, action) {
// here it looks for reducers that actions provide and if not found it returns the state
} 

Now if we set the store state in meta reducer before this feature store gets set up, then the initialState will never be used. (technically 'never' is probably not correct - maybe there is some special case that will use it, like action that will just set the initialState again)

So the rehydration sets the state recovered from LocalStorage as the initial state, then the state defined in initialState of feature modules will never be applied. And only state pieces that are set as a result of subsequent actions will populate rest of the store.

Why is this bad ?

If you have any key defined in initialState of any store it will go missing until you fire an action that will set it explicitly. This is true for any substore that matches top level of any keys defined in the config. I think it is very easy to find a real life example when this is bad.

Is there any chance to have it fixed ?

Slim

One could try to merge rehydrated state with initial state of each store. But I can't foresee the unexpected side effects at this point.

const featureStoreReducer = createReducer(
  initialState,
  on({ type: '@ngrx/store/update-reducers' } as any, (state) => {
    const item = JSON.parse(localStorage.getItem(`ngrx-sync-appState`) || '{}');
    return { ...state, ...initialState, ...item };
  }),
...
BBlackwo commented 1 week ago

Hi @MikeDabrowski thanks for the thorough issue and investigation.

There are several issues with feature stores, and various workarounds mentioned in those issues as well. There are a few PRs with suggested fixes too.

This project is mostly in maintenance mode at this point, so I'm hesitant to do any large changes. There isn't enough test coverage to be confident in changes, and it's hard to get feedback from enough users.