npm install mobx-mc
yarn add mobx-mc
Mobx MC is a library inspired by both Backbone and the approach of using domain stores and domain objects as described in the Mobx Best Practices Documentation.
Mobx MC:
- Separates your domain state from your global state.
- Makes the data observable so you can track values and derive from it.
- Communicates with the server to fetch, save, and delete.
- Manages observable states like fetching, saving, and deleting.
The basic concept is that of a Model
and a Collection
of models. State is managed automatically, and CRUD is built-in. A classic example would be a to-do list, where each todo would be a model and the list of todos would be a collection.
Mobx MC is pre-configured to sync with a RESTful API. Simply create a new Collection with the url of your resource endpoint:
class Todo extends Model {}
class Todos extends Collection {
model() {
return Todo;
}
url() {
return '/todos';
}
}
The Collection and Model components together form a direct mapping of REST resources using the following methods:
GET /todos .... collection.fetch();
POST /todos .... collection.create();
GET /todos/1 ... model.fetch();
PATCH /todos/1 ... model.save();
DELETE /todos/1 ... model.destroy();
- Model
- save(data, options)
- destroy(options)
- Collection
- Where is it used?
- License
To create a Model class of your own, you extend Model and provide instance properties and options for your class. Typically, this is when you'll define the restAttributes
, and any computed properties or actions to be attached to instances of your class.
import { computed } from 'mobx';
import { Model } from 'mobx-mc';
class User extends Model {
get restAttributes() {
return ['firstName', 'lastName'];
}
@computed get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
When creating an instance of a model, you can pass in the initial values of the attributes. You will need to have defined these attributes in restAttributes
.
const me = new User({
firstName: 'Tony',
lastName: 'Stark'
});
As a second argument you can pass in a configuration object with options to pass through to the set
method and the applyOptions
method used to set up references to other class instances.
import rootStore from 'stores/index';
const me = new User(
{
firstName: 'Tony',
lastName: 'Stark'
},
{
parse: true,
rootStore: rootStore
}
);
Override this method to customise how you would like to handle any additional options passed in when a model is initialized.
applyOptions(options) {
if (options.rootStore) {
this.rootStore = options.rootStore;
}
}
Defines a white-list of fields that you expect to receive from your backend API. Any fields not defined here will be stripped out when reading or writing from the server (however this can be overridden by setting the stripNonRest: false
option on any CRUD method).
import { computed } from 'mobx';
import { Model } from 'mobx-mc';
class User extends Model {
get restAttributes() {
return ['firstName', 'lastName'];
}
}
const user = new User();
// Only firstName and lastName sent in request
user.save({
title: 'Mr',
firstName: 'Tony',
lastName: 'Stark'
});
// title, firstName and lastName sent in request
user.save(
{
title: 'Mr',
firstName: 'Tony',
lastName: 'Stark'
},
{
stripNonRest: false
}
);
The attributes property is a reference to a Mobx Observable Map that holds the values for the fields defined in restAttributes
. The model's set
method will keep this map updated when fetching and saving data to the server.
You can use any of the methods available for Maps, or access properties directly using dot syntax because the model provides dynamic getter/setters to the map's keys.
import { computed } from 'mobx';
import { Model } from 'mobx-mc';
class User extends Model {
get restAttributes() {
return ['firstName', 'lastName'];
}
@computed get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const user = new User();
// Use the underlying map's methods
user.attributes.set('firstName', 'Peter');
// Alternatively modify properties directly
user.lastName = 'Parker';
console.log(user.firstName); // 'Peter'
console.log(user.attributes.get('lastName')); // 'Parker'
// The map is observable so anything that referenes the keys will be stay up to date
console.log(user.fullName); // 'Peter Parker'
Sets a value, or multiple values, on the attributes
map. The Model's constructor and CRUD related methods (save,
fetch
etc) call this method when new data is received.
-
parse
(Boolean) - If{parse: true}
is passed as an option, thedata
will first be run through the model'sparse()
method before beingset
on the map. The default for this option istrue
. -
stripNonRest
(Boolean) - If{stripNonRest: false}
is passed as an option, keys that are not specified inrestAttributes
will still be set on theattributes
map. The default for this option istrue
. -
reset
(Boolean) - If{reset: true}
is passed as an option, the entireattributes
map will be reset with the passed indata
(Equivalent ofattributes.clear()
&&attributes.merge(data)
). The default for this option isfalse
, which results in a merge operation (Equivalent ofattributes.merge(data
)). See the Mobx documentation on Maps for more information.
Called internally by the set
method before applying data
to the attributes
map.
The default implementation is a no-op, simply passing through the data
. Override this if your data needs to be modified, remapped, renamed, etc.
class User extends Model {
get restAttributes() {
return ['firstName', 'lastName'];
}
parse(data) {
// Use the parse method to remap the company data into a company model
this.company = new Company(data.company);
// Remove the company from the data.
delete data.company;
// First name and last name will be set on the attributes.
return data;
}
}
Clear all the attributes from attributes
map. Equivalent of calling model.attributes.clear()
Returns a deep plain object representation of the attributes
map.
Use this to define default values for your model. If the value for a key in attributes
is set to undefined
it will fall back to the value specified here.
class User extends Model {
get restAttributes() {
return ['title', 'firstName', 'lastName'];
}
get restAttributeDefaults() {
return {
title: 'Dr'
};
}
}
const user = new User();
console.log(user.title); // 'Dr';
Returns the attribute that should be used as the unique id of the model. This is used to determine the id
when constructing a model's url
for saving to the server.
Defaults to 'id'
.
class User extends Model {
urlRoot = '/users';
idAttribute() {
return 'uuid';
}
get restAttributes() {
return ['firstName', 'lastName'];
}
}
const me = new User({ uuid: 'b5eb81ef-26ff-4df7-bfa3-0d1b7feccbc1' });
console.log(me.url()); // '/users/b5eb81ef-26ff-4df7-bfa3-0d1b7feccbc1'
A unique identifier automatically assigned to all models when they are first created. Client IDs are handy when the model has not been saved to the server so does not yet have its true id
, but still needs a unique id (e.g for usage as a key
when mapping over and rendering React Components).
const user = new User();
console.log(user.cid); // '6b910175-1f56-48de-9fae-0e152629d535'
Returns the model's id
if it's available, otherwise falling back to the cid
. This proerpty is provided for convienence so you don't have to write conditionals throughout your codebase.
render() {
return this.props.users.map(user => <User key={user.uniqueId} />)
}
Has this model been saved to the server yet? If the model does not yet have an id
, it is considered to be new.
A reference to the collection the model belongs to, if in a collection. This is used for building the default url
for a model.
The base url to use for fetching this model. This is useful if the model is not in a collection and you still want to set a fixed "root" but have a dynamic model.url().
class User extends Model {
urlRoot = '/users';
}
const user = new User({
id: 1
});
console.log(user.url()); // '/users/1'
The relative url that the model should use to edit the resource on the server. By default, url
is constructed by finding the model's urlRoot
or the model's collection url
, then appending the idAttribute
. However, if the model does not follow normal REST endpoint conventions, you may overwrite it. In such a case, url
may be absolute.
class User extends Model {
urlRoot = '/users';
}
const user = new User({
id: 1
});
console.log(user.url()); // '/users/1'
class Me extends User {
url() {
return '/me''
}
}
const me = new Me({
id: 2
})
console.log(me.url()); // '/me'
Mobx MC relies on the Axios library for making http requests. All CRUD related methods return a promise that will resolve to either the model instance or an error object.
You can pass in Axios specific configuration by passing an additional axios
object inside options
for any method.
model
.fetch({
axios: {
timeout: 1000
}
})
.then(model => {})
.catch(error => {});
Resets the model's attributes
with data fetched from the server. Useful if the model has never been populated with data, or if you'd like to ensure that you have the latest server state.
Calling model.fetch
will toggle a fetching
observable property so you can respond accordingly to the status of the http GET
request (e.g. To show a loading animation).
params
(Object) - Used to dynamically add query parameters to the url when fetching.url
(String) - On some occasions it may be desirable to override theurl
for a single request. The request will default tomodel.url()
when this is not explicitly configured.
In addition to the above, you can also pass in any options supported by the set
method and these will be passed through to that method when handling the response from the server.
class User extends Model {
urlRoot = '/users';
}
const user = new User({
id: 1
});
user.fetch(); // GET request to '/users/1'
user.fetch({
url: '/me'
params: {
includeRole: true
}
}); // GET request to '/me?includeRole=true'
The data
argument should contain the attributes you'd like to change - attributes that aren't provided will not be sent in the resulting request. You can pass null
to send a copy of all the attributes currently in memory.
If the model isNew
, the save will be a "create" (POST
). If the model already exists on the server, the save will be an "update" (PATCH
).
Calling model.save
will toggle a saving
observable property so you can respond accordingly to the status of the http request.
wait
(Boolean) - Pass{wait: true}
if you'd like to wait for the server to respond before updating the model's attributes. The default for this option isfalse
which performs an optimistic update.method
(String) - Passed down to Axios to override the request method. e.g. Your API may require aPUT
request so you could passmethod: PUT
in the options when callingsave
.url
(String) - On some occasions it may be desirable to override theurl
for a single request. The request will default tomodel.url()
when this is not explicitly configured.
In addition to the above, you can also pass in any options supported by the set
method and these will be passed through to that method when handling the response from the server.
const user = new User({
firstName: 'Clark',
lastName: 'Kent'
});
user.save();
// Sends a POST request to /users with all of the model's attributes
{
"firstName": "Clark",
"lastName": "Kent"
}
// Subsequent saves update the model
user.save({
firstName: "Super"
});
// Sends a PATCH request to /users/1
{
"firstName": "Super"
}
Sends an DELETE
request to the model's URL, and if the request is successful it also removes the model from the collection it belongs to.
Calling model.delete
will toggle a deleting
observable property so you can respond accordingly to the status of the http request.
- Pass
{wait: true}
if you'd like to wait for the server to respond before removing the model from it's collection.
user.destroy(); // Sends a DELETE request to the model's url.
To create a Collection class of your own, you extend Collection and provide instance properties and options for your class. Typically, this is when you'll define the url
for the related resource and a model
class that you would like your collection to contain.
import { computed } from 'mobx';
import { Collection } from 'mobx-mc';
class Users extends Collection {
url() {
return '/users';
}
model() {
return User;
}
}
When creating an instance of a collection you can pass in a data object (or JSON) to populate the models.
const users = new Users([
{
id: '1',
firstName: 'Clark',
lastName: 'Kent'
},
{
id: '2',
firstName: 'Tony',
lastName: 'Stark'
}
]);
console.log(users.length); // 2
As a second argument you can pass in a configuration object with options to pass through to the set
method, and the applyOptions
method used to set up references to other class instances.
import rootStore from 'stores/index';
const users = new Users(
[
{
id: '1',
firstName: 'Clark',
lastName: 'Kent'
},
{
id: '2',
firstName: 'Tony',
lastName: 'Stark'
}
],
{
rootStore: rootStore
}
);
Override this method to customize how you would like to handle any additional options passed in when a collection is initialized.
applyOptions(options) {
if (options.rootStore) {
this.rootStore = options.rootStore;
}
}
Override this method to specify the model class that the collection contains. If defined, you can pass raw objects (and arrays) to add
and reset
, and the attributes will be converted into a model of the proper type.
The models property is a reference to a Mobx Observable Array that holds an instance of each model in the collection. The collection's set
method will keep this array updated when fetching and saving data to the server.
You can use any of the methods available for ES6 Arrays and Mobx Arrays.
Shortcut property equivalent to models.length
The set method performs a "smart" update of the the models array with the passed list of models:
- If a model in the list isn't in the collection, it will be added.
- If the collection contains any models that aren't in the list, they'll be removed.
- If a model in the list is in the collection already, its attributes will be updated.
If you'd like to customize the behavior, you can configure it with options:
add
(Boolean)remove
(Boolean)update
(Boolean)
When using the update
option, the default behaviour is a full reset of each model's attributes, deleting any keys not specifed in data
. Pass merge:true
if you would like to merge in the data
and keep all existing keys in-tact.
merge
(Boolean)
Called internally by the set
method before applying data
to the models
array.
The default implementation is a no-op, simply passing through the data
. Override this if your data needs to be modified, remapped, renamed, etc.
class Users extends Collection {
model() {
return User;
}
parse(data) {
return data.collection;
}
}
const users = new Users({
collection: [
{
id: '1',
firstName: 'Clark',
lastName: 'Kent'
},
{
id: '2',
firstName: 'Tony',
lastName: 'Stark'
}
]
});
console.log(users.length); // 2
Get a model from a collection, specified by an id
or cid
.
const users = new Users([
{
id: '1',
firstName: 'Clark',
lastName: 'Kent'
},
{
id: '2',
firstName: 'Tony',
lastName: 'Stark'
}
]);
console.log(users.get('2').firstName); // Tony
Get a model from a collection, specified by index
. Equivalent of collection.models[index]
. i.e. collection.at(0)
returns the first model in the collection.
Add a model (or an array of models) to the collection. If a Model class is defined, you may also pass raw model data and options, and have them be vivified as instances of the model using the provided options. Returns the updated models
array.
at
Pass{at: index}
to splice the model into the collection at the specifiedindex
You can also pass in any options supported by the set
method and these will be passed through to that method when adding the new models to the collection.
Attempts to find a model with the same idAttribute
passed in the data
object and return it. If no pre-existing model is found, it will add the new model, then return it.
Remove a model (or an array of models) from the collection. The models object/array can be references to actual models, or raw data objects.
Call reset(array)
to replace a collection with a new list of models (or attribute hashes). Calling collection.reset()
without passing any models as arguments will empty the entire collection.
Set the url method on a collection to reference its location on the server. Models within the collection will use url to construct URLs of their own.
import { computed } from 'mobx';
import { Collection } from 'mobx-mc';
class Users extends Collection {
url() {
return '/users';
}
model() {
return User;
}
}
const users = new Users();
const user = users.add({
{
"firstName": "Clark",
"lastName": "Kent"
}
});
user.save(); // POST request sent to /users
users.fetch() // GET request sent to /users
Mobx MC relies on the Axios library for making http requests. All CRUD related methods return a promise that will resolve to either the collection, newly created model instance or an error object.
You can pass in Axios specific configuration by passing an additional axios
object inside options
for any method.
Fetch the a set of models for the collection from the server, setting them on the collection's models
array when they arrive.
You can pass in any options supported by the set
method and these will be passed through to that method when handling the response from the server.
Calling model.fetch
will toggle a fetching
observable property so you can respond accordingly to the status of the http GET
request (e.g. To show a loading animation).
params
(Object) - Used to dynamically add query parameters to the url when fetching.url
(String) - On some occasions it may be desirable to override theurl
for a single request. The request will default tocollection.url()
when this is not explicitly configured.
users.fetch({
params: {
page: 2
}
});
// GET request to '/users?page=2'.
Convenience to create a new instance of a model within a collection. Equivalent to instantiating a model with new data, saving the model to the server, and adding the model to the collection after being successfully created. Returns a promise that resolves with the newly created model.
- Pass
{wait: true}
if you'd like to wait for the server before adding the new model to the collection. url
(String) - On some occasions it may be desirable to override theurl
for a single request. The request will default tocollection.url()
when this is not explicitly configured.
You can pass in any options supported by the set
method and these will be passed through to that method when handling the response from the server.
const newUser = users.create({
firstName: 'Peter',
lastName: 'Parker'
});
// POST request to '/users'. New model added to the collection
Convenience method. Gets a model from the server or from the collection if a model with that id
already exists.
Developed and tested in production at Raken
MIT
Many of the ideas here should be credited to Jeremy Ashkenas and the rest of the Backbone.js authors.
Thanks to Michel Westrate for Mobx.