Models
Each object type in your GraphQL schema is converted into two things: a BaseModel
class, and a Model
subclass.
The BaseModel
class contains the fields from your schema, as well as some helper methods (we'll get into that later.)
For example, given the following schema:
type User { id: ID! firstName: String! lastName: String! email: String!}
This will generate the following BaseModel
:
// Do not edit any BaseModel! They are generated automatically and will be overwritten.export class UserBaseModel { @Selectable() declare id: ID; @Selectable() declare firstName: string; @Selectable() declare lastName: string; @Selectable() declare email: string; // ... constructor, helper methods, etc...}
And the following Model
:
// Feel free to add any fields or methods! Try not to touch the constructor, however.export class UserModel extends UserBaseModel { constructor(init: Partial<UserModel> = {}) { super(init); makeModelObservable(this); }}
You can instantiate a UserModel
like so:
const user = new UserModel({ firstName: "John", lastName: "Doe", email: "johndoe@example.com"});
Adding logic to a model
You can add any fields or methods to your Model
class. For example, you can add a fullName
computed property:
export class UserModel extends UserBaseModel { constructor(init: Partial<UserModel> = {}) { super(init); makeModelObservable(this); } get fullName() { return `${this.firstName} ${this.lastName}`; }}
This is super valuable because any data returned from mutations and queries will get deeply converted into
their respective Model
instances. This means you can conveniently access any fields or methods you've added to your
Model
class.
Mutating data within a model
You can mutate data within a model by using the built-in set
method:
const user = new UserModel({});user.set('firstName', 'John');user.set('lastName', 'Doe');
You can also use the assign
method for merging an object into the instance:
const user = new UserModel({});user.assign({ firstName: 'John', lastName: 'Doe' });
What is @Selectable
?
The @Selectable
decorator adds logic that throws an error if you try to access a field that was not selected from a
query. This helps you avoid optional chaining and other null checks all over your code.
For example, given the following query:
query { user { id name }}
If you try to access user.email
, you'll get an error:
// Imagine `user` is a `UserModel` instance that was returned from a queryuser.email; // Error: Field "email" is not selected
This is ideal, as it keeps your code clean and prevents your logic from silently producing invalid results.
Getting selected data
If you'd like to get an object containing the data that has been selected, you can use the selectedData
getter:
const userQuery = new UserQuery(user => user.name.email);userQuery.dispatch().then(user => { console.log(user.selectedData); // { id: '1', name: 'John', email: 'johndoe@example.com' }});
What is makeModelObservable
?
You may have noticed that generated models use makeModelObservable
from mobx-depot
instead of makeAutoObservable
.
MobX has limitations regarding the use of makeAutoObservable
with subclasses. makeModelObservable
works around this
limitation by determining the required annotations by traversing over this
and its prototypes, then providing them to
makeObservable
.
Do not use this in your own code. This works for us because:
- We're not extending an external class
BaseModel
s are designed to be inherited from, and are not meant to be instantiated directly
See relevant MobX discussion for more details.
wontfix
I'd like to emphasize @urugator's comment:
[...] this is not a suggested way to go, therefore I don't want to promote it in any way. Whatever you expose, some people will use it, twist it, misuse it, and in the end it will always come back to you, no matter the warnings. Automatic behavior provides convenience for common/simple cases. If it can't support inheritance reliably, the responsible thing to do is either not introducing automatic behavior in the first place (no longer an option) or to forbid cases that could lead to issues.
I am quite convinced that the cost of using makeObservable instead of makeAutoObservable (or even better getting rid of inheritance) for these ~10 classes , will be lower than the cost of introducing and maintaining any workaround.
Any usage of makeModelObservable
outside of the sole purpose of extending BaseModel
s is unsupported. Any issues
regarding non-generated usage of makeModelObservable
will be closed as wontfix
.
I have yet to find a case where this doesn't work. If you do, please open an issue!