Skip to content

Changing schema of state - guidance on versioning/migrations? #147

@kylekampy

Description

@kylekampy

Hello! Thanks so much for creating this utility! It is so useful and I very much appreciate it.

I ran into a small problem, which is definitely self-inflicted I just didn't realize I was going to encounter it.

I've got state persisted to local storage. There are various top-level keys within the state that it's writing to storage. I'd like to add an additional key. As an example, currently I have:

createStore(
    {
      thing1: getThing1InitialState(), // returns some object with default values
      thing2: getThing2InitialState(),
    },
);

And I want to add a new thing. My code now looks like:

createStore(
    {
      thing1: getThing1InitialState(),
      thing2: getThing2InitialState(),
      thing3: getThing3InitialState(), // <-- new
    },
);

Then my components have:

const { state } = useStateMachine();

const propertyOfThing3 = state.thing3.foo; // unexpected -- cannot access property 'foo' of undefined

Because I already had a JSON bit of state written to local storage before releasing a version with thing3, the JSON blob is parsed and used as state on app load through the eventual calls to updateStore https://github.com/beekai-oss/little-state-machine/blob/master/src/logic/storeFactory.ts#L20. Because that older version of state didn't have thing3, it's just not included in there, and there doesn't appear to be logic in little-state-machine to "fill in the blanks" with default values. Then state.thing3 is undefined and things crash.

The way I solved this for myself was in assuming any of the pieces of state could be undefined. I have my GlobalState type defined like:

declare module 'little-state-machine' {
  interface GlobalState {
    thing1?: Thing1Type;
    thing2?: Thing2Type;
    thing3?: Thing3Type;
  }
}

And then my actions all take special precaution to assume they may be working with nested state that isn't yet defined:

export function updateThing3Foo(state: GlobalState, payload: {
  newFoo: string,
}): GlobalState {
  return {
    ...state,
    thing3: {
      ...getThing3InitialState(), // <-- in case we've added new properties, or this whole state hasn't yet been written/parsed
      ...state.thing3, // <-- in case there was already state, and we don't want to lose any of the other properties that were here
      foo: payload.newFoo,
    },
  };
}

I figured I'd write this out in case anyone else has also encountered this problem and is looking for some kind of solution that seems to work. But I would also love to hear from others on where I went wrong. How are others adding to their state over time without crashes on release? Does anyone have a migration or versioning strategy they use to keep their typing of state and actual state consistent?

Thanks so much!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions