Mobx-state-tree: Bidirectional references

Created on 2 Jan 2018  路  6Comments  路  Source: mobxjs/mobx-state-tree

References seem to be a really nice way to define relationships between models. However, I cannot find any explanation or pattern for managing bidirectional references.

For example, imagine a User, Group and Membership model where Membership has references to a User and a Group. A typical use case would involve showing all the memberships of a particular user, or a particular group. All three models would be stored in a map inside an overall Store.

How can we define such situation in MST? Would it be useful to add an example to the docs (preferably using TypeScript)?

I've tried getParent() to link to a root store and then apply filtering (e.g. on User, define a view "get memberships" that does getParent, then goes to all memberships, then filters on member.user == self . However, already by using getParent we loose type checking as the parent type cannot be resolved at compile time.

Thanks for the help, keep up the good work!!

Most helpful comment

I have several ideas how to do it, but all of them involving using getParent

First you can use getParent in a kinda "safe" way

interface IParentInterface {
  associations: Array<{userId: string; membershipId: string; groupId: string; }>;
}

.views(self => {
  return {
    get _getParent(): IParentInterface {
      // make some runtime validations if you feel the need
      return getParent(1 /* or more */, self);
    }
  };
}).views(self => {
  return {
    get associations() {
      // make some runtime validations
      if (!self._getParent.associations) {
        throw new Error("oopppss. where's your daddy?");
      }

      return self._getParent.associations;
    },
  }
});

But we still want to avoid direct circular refs, so we just keep the associations as ids,
And lets say you are on the Group model and you want to get all your users:

.views(self => {
  return {
    get users(): Array<typeof UserModel.Type> {
      return self.associations.filter(a => a.groupId === self.id).map(a => resolveIdentifier(UserModel, getRoot(self), a.userId));
    },
  }
});

All 6 comments

I have several ideas how to do it, but all of them involving using getParent

First you can use getParent in a kinda "safe" way

interface IParentInterface {
  associations: Array<{userId: string; membershipId: string; groupId: string; }>;
}

.views(self => {
  return {
    get _getParent(): IParentInterface {
      // make some runtime validations if you feel the need
      return getParent(1 /* or more */, self);
    }
  };
}).views(self => {
  return {
    get associations() {
      // make some runtime validations
      if (!self._getParent.associations) {
        throw new Error("oopppss. where's your daddy?");
      }

      return self._getParent.associations;
    },
  }
});

But we still want to avoid direct circular refs, so we just keep the associations as ids,
And lets say you are on the Group model and you want to get all your users:

.views(self => {
  return {
    get users(): Array<typeof UserModel.Type> {
      return self.associations.filter(a => a.groupId === self.id).map(a => resolveIdentifier(UserModel, getRoot(self), a.userId));
    },
  }
});

BTW on mobx, i also using tree-shaped models, and i'm using explicit dependency injections.
With mobx i would inject the associations array and users array, or "usersRegistryService"

on MST you don't need explicit dependency injections because you can traverse up the tree

this is possible using normal references and a late type.

within UserModel:
types.reference(types.late(() => Group))
within GroupModel:
types.reference(types.late(() => User))

https://codesandbox.io/s/wyr0pj0lnl

@robinfehr any way to make that work with typescript?

The any's seem to kill the types for the IDE.

@ConneXNL this might help you - #417

@robinfehr The example you provided doesn't work.
The reference from Group back to user is undefined.

It looks like the model is incorrect, because it is GroupModel.group , whereas it should be .user.
Changing it to user throws an error.

Was this page helpful?
0 / 5 - 0 ratings