Aurelia Store - Initial state and local storage

Hi all,

I’ve been using Aurelia Store to persist my local state into local storage.

I’ve noticed that if any changes are made to my initialState declaration (e.g new properties, or changes to default values), they are discarded when the rehydrateFromLocalStorage action is dispatched on refresh.

My current approach is :

export class App() {
    constructor(private store: Store<State>) {
        // Register the middleware
         store.registerMiddleware(localStorageMiddleware, MiddlewarePlacement.After, {key: 'storage-key' });
        
        // Register the rehydration action
        store.registerAction('Rehydrate', rehydrateFromLocalStorage);

        // ... Other state actions declared here

        dispatchify('Rehydrate')('storage-key');
    }
}

If I were to subscribe to the state, add a new property to the initial state, and refresh - I can see that the new initial state is loaded correctly until the rehydrate action is dispatched.

At this point the state subscriber returns exactly what was stored in local storage before the changes, excluding any new/updated properties or default values.

Do I need to change the way I’m configuring the plugin, or is there a standard practice for managing changes like this?

Thanks in advance for your time!

Cheers,
Tom

1 Like

That would require the store to know how to diff/merge the two states which is a very generic task. In this case I’d recommend to build your own rehydrate action and do the sanitization/merging right in there

so something along the lines of

import {rehydrateFromLocalStorage} from "aurelia-store";

export function customRehydrate(state: State, key?: string) {
   const newState = rehydrateFromLocalStorage(state, key);

   // modify newState according to your needs but keep the shallow cloning in mind

  return newState;
}
3 Likes

I had the same problem and came up with

import { autoinject } from "aurelia-framework";
import { Store } from "aurelia-store";
import _ from "lodash";
import { initialState } from "./initial-state";
import { IState } from "./state";

@autoinject
export class StateInitializationService {
  constructor(
    private store: Store<IState>
  ) {
    store.registerAction("initializeState", initializeStateAction);
  }

  public init(state: IState) {
    Object.keys(initialState).forEach(key => {
      if (state && state[key] === undefined) {
        state[key] = initialState[key];
      }
      if (initialState[key]?.hasOwnProperty("current") && !state[key].current) {
        state[key].current = initialState[key].current;
      }
    });

    state.serverMessages.errorMessage = undefined;
    state.serverMessages.message = undefined;

    this.store.dispatch(initializeStateAction, state);
  }
}

export function initializeStateAction(state: IState, response: IState) {
  let newState = _.cloneDeep(state);
  newState = response;
  return newState;
}

app.ts

constructor(
    store: Store<IState>
  ) {
       store.registerAction("Rehydrate", rehydrateFromLocalStorage);

       if (localStorage[LOCAL_STORAGE.state]) {
      store.dispatch(rehydrateFromLocalStorage, LOCAL_STORAGE.state);
    }

    store.registerMiddleware(localStorageMiddleware, MiddlewarePlacement.After, { key: LOCAL_STORAGE.state });
  }

protected bind() {
    this.stateInitializationService.init(this.state);
  }
4 Likes

Thanks for the advice, zewa666 and jeremyholt.

I’ve ended up using a combination of both of these suggestions :

export function customRehydrateAction(state: State, key?: string) {
    return syncState(
        {... state},
        rehydrateFromLocalStorage(state, key)
    ) as State;
}

// Recursively syncs two given state objects
function syncState(initialState: {},  fromStorage: {}) {
    Object.keys(initialState).forEach(key => {
        // If we don't have a value in our local storage, leave the initial value as is.
        if (!(key in fromStorage)) {
            return;
        }

        // If both keys are Objects, excluding arrays, recursively sync the entries of each.
        if (
            isObject(initialState[key]) &&
            isObject(fromStorage[key]) &&
            !Array.isArray(initialState[key])
        ) {
            initialState[key] = syncState(initialState[key], fromStorage[key]);
            return; 
        }

        // If we get here, just assign our stored value to our state.
        initialState[key] = fromStorage[key];
    });

    return initialState;
}

This appears to be working nicely, and has the added benefits of clearing out keys and values which are no longer present in the initialState.

Thanks again for your help!

3 Likes