Mobx-state-tree: Dynamic models

Created on 31 Jan 2019  Â·  13Comments  Â·  Source: mobxjs/mobx-state-tree


_Question_

  • [ ] I've checked documentation and searched for existing issues

reference

What is the best way to define model for below object? Dynamic reference and each object in the array is a separate model?

array: [
    {
      id: "num1",
      type: "type1",
      sum: ""
    },
    {
      id: "num2",
      type: "type1",
      sum: ""
    },
    {
      id: "num3",
      type: "type2",
      data: "",
      array: []
    }
  ]
question stale

Most helpful comment

Btw, I just wrote this unit test that passed, so as long as the union items use a different ID then the reference will work

test("#1162 - reference to union", () => {
    const M1 = types.model({ id: types.identifier, type: types.string, sum: types.string })
    const M2 = types.model({
        id: types.identifier,
        type: types.string,
        data: types.string
    })
    const AnyModel = types.union(
        {
            dispatcher(snapshot) {
                switch (snapshot.type) {
                    case "type1":
                        return M1
                    case "type2":
                        return M2
                    default:
                        throw new Error()
                }
            }
        },
        M1,
        M2
    )

    const Store = types.model({
        arr: types.array(AnyModel),
        selected: types.reference(AnyModel)
    })

    const s = Store.create({
        selected: "num1",
        arr: [
            { id: "num1", type: "type1", sum: "1" },
            { id: "num2", type: "type1", sum: "2" },
            { id: "num3", type: "type2", data: "3" }
        ]
    })
    unprotect(s)

    expect(s.selected.id).toBe("num1")
    expect(s.selected.type).toBe("type1")
    expect((s.selected as Instance<typeof M1>).sum).toBe("1")

    s.selected = "num2" as any
    expect(s.selected.id).toBe("num2")
    expect(s.selected.type).toBe("type1")
    expect((s.selected as Instance<typeof M1>).sum).toBe("2")

    s.selected = "num3" as any
    expect(s.selected.id).toBe("num3")
    expect(s.selected.type).toBe("type2")
    expect((s.selected as Instance<typeof M2>).data).toBe("3")
})

so most probably the error you saw was that the reference was pointing to a node that was deleted. In this case consider using types.safeReference instead

All 13 comments

Depends on what you are trying to model.
Could you provide a typescript definition of what you are trying to achieve?

At a glance, you may want to look at unions

On Fri, Feb 1, 2019, 7:40 AM Javier Gonzalez notifications@github.com
wrote:

Depends on what you are trying to model.
Could you provide a typescript definition of what you are trying to
achieve?

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx-state-tree/issues/1162#issuecomment-459725545,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAIrciweKgAUf8N8FczCA2TiG9QkjAb6ks5vJEO3gaJpZM4adIMa
.

I will try to explain this. There are models with a set of their actions.

const model1 = types.model({
  id: types.identifier,
  type: types.string,
  sum: types.string
}).actions(...);

const model2 = types.model({
  id: types.identifier,
  type: types.string,
  data: types.string,
  lox: types.string
}).actions(...);

const Skus = types.model({
  // here it is necessary to describe an array in which there would be 
  // references to models model1, model2, model3, etc. depending on 
  // the value of the type field.
}).actions(...);

const output = Skus.create({
  arr: [
    {
      id: "num1",
      type: "type1",
      sum: ""
    },
    {
      id: "num2",
      type: "type1",
      sum: ""
    },
    {
      id: "num3",
      type: "type2",
      data: "",
      lox: ""
    }
  ]
}); 

It turns out an analogue of normalization in the database, but with references to different tables. What are your thoughts on the description of such a structure so as not to spoil the performance?

https://github.com/mobxjs/mobx-state-tree#utility-types

a types.array with a types.union with a dispatcher ( a function that given a snapshot will decide which type it is)

const anyModel = types.union({
  dispatcher(snapshot) {
    switch (snapshot.type) {
      case "type1": return model1;
      case "type2": return model2;
      default: throw new Error();
    }
  },
  model1, model2
});

const Skus = types.model({
  array: types.array(anyModel)

btw, bear in mind that the snapshot representation of references are string/number, this is, they are just the ids of the referenced object, not the object itself

Great solution! Thanks a lot! I did not know about such features dispatcher.

And how to specify references to these model (model1, model2, etc.) so that you can refer to them?

const Skus = types.model({
  arr: types.array(anyModel),
  selected: // ???  //  types.reference(anyModel) - this option will not work
});

const output = Skus.create({
  selected: "num1",
  arr: [
    {
      id: "num1",
      type: "type1",
      sum: ""
    },
    {
      id: "num2",
      type: "type1",
      sum: ""
    },
    {
      id: "num3",
      type: "type2",
      text: "text"
    }
  ]
});

types.maybe(types.reference(anymodel)) or types. safeReference(anymodel) depending on the use case

Although now that I think about it I'm not sure references work with unions, so another option is using a property to store the selected ID and a view that resolves the actual instance

Yes, references is not work... I see this error in the console

console.log(output.selected);

mobx-state-tree.module.js?f7d3:5207 Uncaught Error: [mobx-state-tree] Failed to resolve reference 'num1' to type '()' (from node: /selected)
    at new InvalidReferenceError$$1 (mobx-state-tree.module.js?f7d3:5207)
    at StoredReference.updateResolvedReference (mobx-state-tree.module.js?f7d3:5182)
    at StoredReference.get [as resolvedValue] (mobx-state-tree.module.js?f7d3:5192)
    at IdentifierReferenceType$$1.getValue (mobx-state-tree.module.js?f7d3:5376)
    at ScalarNode$$1.get [as value] (mobx-state-tree.module.js?f7d3:1174)
    at ObservableValue$$1.ObjectNode$$1._this.unbox [as dehancer] (mobx-state-tree.module.js?f7d3:1253)
    at ObservableValue$$1.dehanceValue (mobx.module.js?daf9:672)
    at ObservableValue$$1.get (mobx.module.js?daf9:725)
    at ObservableObjectAdministration$$1.read (mobx.module.js?daf9:3532)
    at Object.get [as selected] (mobx.module.js?daf9:3773)

You can write an example of another option with the property to store the selected ID and a view that resolves the actual instance?

const Skus = types.model({
  arr: types.array(anyModel),
  selectedId: types.maybe(types.string)
}).views(self => ({
  get selected(): Instance<typeof anyModel> | undefined { return self.selectedId ? self.arr.find(item => item.id === self.selectedId) : undefined }
})).actions(self => ({
  setSelected(item: Instance<typeof anyModel> | undefined) { self.selectedId = item ? item.id : undefined }
});

something like that

This is another option, but using find to search for an identifier is irrational, it seems to me. It’s pointless to check all array elements every time, especially if the array is large. Then it's easier to use types.map

const Reference = types.reference(anyModel, {
  get(identifier, parent) {
    return parent.arr.find(item => item.id === identifier);
  },
  set(value) {
    return value;
  }
});

const Skus = types.model({
  arr: types.array(anyModel),
  selected: Reference
});

And how is the reference mechanics inside the library mobx-state-tree?

Basically there's a reference registry per root store, which is a map like Map<type, id>.
That's why each root store cannot have more than one id per type.

When resolveIdentifier(type, target, identifier) is used, it will actually go to the root node of the target node to find that map and then do a getRoot(target)->identifierMap.get(type, identifier)

When a new node with an identifier is added to the root store it will do getRoot(self)->identifierMap.set(self type, identifier)

There's more to it,, but that's kind of how it works.

Of course another way is to just prefix the id with the type before adding it to the store, then it should work with plain references (could be done with a pre/postSnapshotProcessor)

Btw, I just wrote this unit test that passed, so as long as the union items use a different ID then the reference will work

test("#1162 - reference to union", () => {
    const M1 = types.model({ id: types.identifier, type: types.string, sum: types.string })
    const M2 = types.model({
        id: types.identifier,
        type: types.string,
        data: types.string
    })
    const AnyModel = types.union(
        {
            dispatcher(snapshot) {
                switch (snapshot.type) {
                    case "type1":
                        return M1
                    case "type2":
                        return M2
                    default:
                        throw new Error()
                }
            }
        },
        M1,
        M2
    )

    const Store = types.model({
        arr: types.array(AnyModel),
        selected: types.reference(AnyModel)
    })

    const s = Store.create({
        selected: "num1",
        arr: [
            { id: "num1", type: "type1", sum: "1" },
            { id: "num2", type: "type1", sum: "2" },
            { id: "num3", type: "type2", data: "3" }
        ]
    })
    unprotect(s)

    expect(s.selected.id).toBe("num1")
    expect(s.selected.type).toBe("type1")
    expect((s.selected as Instance<typeof M1>).sum).toBe("1")

    s.selected = "num2" as any
    expect(s.selected.id).toBe("num2")
    expect(s.selected.type).toBe("type1")
    expect((s.selected as Instance<typeof M1>).sum).toBe("2")

    s.selected = "num3" as any
    expect(s.selected.id).toBe("num3")
    expect(s.selected.type).toBe("type2")
    expect((s.selected as Instance<typeof M2>).data).toBe("3")
})

so most probably the error you saw was that the reference was pointing to a node that was deleted. In this case consider using types.safeReference instead

This issue has been automatically marked as stale because it has not had recent activity in the last 10 days. It will be closed in 4 days if no further activity occurs. Thank you for your contributions.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or questions.

Was this page helpful?
0 / 5 - 0 ratings