xaviergonz / mobx-keystone

A MobX powered state management solution based on data trees with first class support for Typescript, support for snapshots, patches and much more

Home Page:https://mobx-keystone.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

incorrect detaches from root store when applying snapshot

ZFail opened this issue · comments

commented

this simple test produces multiple onAttachedToRootStore hook calls:

test('reattach', () => {
  let attachCount = 0
  let detachCount = 0
  @model("A")
  class A extends Model({
    id: idProp.withSetter()
  }) {
    protected onAttachedToRootStore() {
      ++attachCount
      return () => {
        ++detachCount
      }
    }
  }

  @model("B")
  class B extends Model({
    id: idProp,
    a: tProp(types.model(A)),
  }) {
  }

  const b = new B({id: 'b', a: new A({id: 'a'})})
  registerRootStore(b)
  const snapshot = getSnapshot(b)
  b.a.setId('a2')
  applySnapshot(b, snapshot)
  unregisterRootStore(b)
  expect(attachCount).toBe(1)
  expect(detachCount).toBe(1)
})

if i remove b.a.setId('a2') or applySnapshot call, this test produces only one onAttachedToRootStore hook call
is this correct or bug? in my case this additional hook calls broke my change state logic (in my code, attach hooks are actively used to initiate different processes and stop them in detach hook)

This is what's happening:

import {
  idProp,
  model,
  Model,
  registerRootStore,
  tProp,
  types,
  getSnapshot,
  applySnapshot,
  unregisterRootStore
} from "mobx-keystone";

console.clear();

@model("A")
class A extends Model({
  id: idProp.withSetter()
}) {
  protected onAttachedToRootStore() {
    console.log("attach");
    return () => {
      console.log("detatch");
    };
  }
}

@model("B")
class B extends Model({
  id: idProp,
  a: tProp(types.model(A))
}) {}

const b = new B({ id: "b", a: new A({ id: "a" }) });

registerRootStore(b);

// attach // when you register B as rootstore then A becomes attached to a "new" rootstore (B in this case)

const snapshot = getSnapshot(b);
// here an old snapshot of B is saved

b.a.setId("a2");
// here a new snapshot of B.A is generated

applySnapshot(b, snapshot);
// this actually creates a new instance of B.A, since the ID of old B.A and new B.A do not match 
// (if you had changed something else other than the ID then it would have been "reconcilied" rather than replaced)
// detatch // old B.A dies
// attach // new B.A is added

unregisterRootStore(b);
// detatch // the rootstore is detached, so B.A is detached from a "root store"
commented

Thank you for detailed answer!
Now i dont change ids, but i also get multiple attach/detach

@model("A")
class A extends Model({
  id: idProp.withSetter(),
  n: tProp(types.number).withSetter()
}) {
  protected onAttachedToRootStore() {
    console.log("attach");
    return () => {
      console.log("detach");
    };
  }
}

@model("B")
class B extends Model({
  id: idProp,
  a: tProp(types.model(A))
}) {}

console.log("1");
const b = new B({ id: "b", a: new A({ id: "a", n: 1 }) });
console.log("2");
registerRootStore(b);
console.log("3");
const snapshot = getSnapshot(b);
console.log("4");
b.a.setN(2);
console.log(getSnapshot(b), snapshot);
applySnapshot(b, snapshot);
console.log("6");
unregisterRootStore(b);
console.log("7");

This is ok? and applySnapshot always detach all models from root store and reattaching them

That last one was indeed a bug. Fixed in 1.6.1

commented

It works, thanks!