How To Split Mobx State Tree Models Across Multiple Files?
Asked Answered
C

2

4

I have a Mobx State Tree model that has grown too long and I would like to split it across multiple javascript files.

Here is a demo of some of the code:

///file1.js
 import { types } from "mobx-state-tree";

export const ExampleModel = types
.model("Example", {
    id: types.identifier,
    name: types.optional(types.string, ""),
    anotherName: types.optional(types.string, ""),

})
.views(self => ({
    get test() {
        return "test"
    }
}))
.views(self => ({
    get anotherTest() {
        return "anotherTest"
    }
}))
.actions(self => ({
    setName(name) {
        self.name = name
    }
}))
.actions(self => ({
    setAnotherName(name) {
        self.anotherName = name
    }
}))

What I want is to split this between two files, like:

///file1.js
import { types } from "mobx-state-tree";

export const ExampleModel = types
.model("Example", {
    id: types.identifier,
    name: types.optional(types.string, ""),
    anotherName: types.optional(types.string, ""),

})
.views(self => ({
    get test() {
        return "test"
    }
})) 
.actions(self => ({
    setName(name) {
        self.name = name
    }
}))


///file2.js
import { ExampleModel } from "./file1.js";
ExampleModel.views(self => ({
    get anotherTest() {
        return "anotherTest"
    }
})).actions(self => ({
    setAnotherName(name) {
        self.anotherName = name
    }
}))

You can see here that I am attempting to move a view and and action to a separate javascript file. I expect I need to do some kind of import and export between these two files, but I can't figure out how to do it.

I know that Mobx State Tree has compose functionality, as shown here: https://nathanbirrell.me/notes/composition-mobx-state-tree/

But I am afer something more simple than this... I don't want to set up multiple models, I just need the ability to spread a model across multiple javascript files.

Creel answered 7/1, 2019 at 19:7 Comment(0)
A
9

We do that all the time.

Just export your actions and views separately:

// file1.js
import { types } from "mobx-state-tree"

export const props = {
    id: types.identifier,
    name: types.optional(types.string, ""),
    anotherName: types.optional(types.string, ""),

}
export const views = self => ({
    get test() {
        return "test"
    }
})
export const actions = self => ({
    setName(name) {
        self.name = name
    }
})

Then, create the final store from them:

// store.js
import { types } from "mobx-state-tree"
import * as file1 from "./file1"
import * as file2 from "./file2"

const Store = types
  .model('Store')
  .props(file1.props)
  .views(file1.views)
  .actions(file1.actions)
  .props(file2.props)
  .views(file2.views)
  .actions(file2.actions)

export default Store

You can also create your own stores for testing, only from one file:

// __tests__/file1.js
import { types } from "mobx-state-tree"
import { actions, views, props } from "./file1"

const Store = types
  .model('Store')
  .props(props)
  .views(views)
  .actions(actions)
const store = Store.create(myTestSnapshot)

test('setName should set the name prop', () => {
  store.setName('john')
  expect(store.name).toBe('john')
})
Asbestosis answered 7/1, 2019 at 20:31 Comment(1)
Amazing answer! Should be added to mobx documentation!Creel
M
3

Expressive, flexible and easy model composition is one of the best features in mobx-state-tree! :) Here are two examples, taken straight from the relevant section in the docs:

const Square = types
    .model("Square", {
        width: types.number
    })
    .views(self => ({
        surface() {
            return self.width * self.width
        }
    }))

// create a new type, based on Square
const Box = Square
    .named("Box")
    .views(self => {
        // save the base implementation of surface
        const superSurface = self.surface
        return {
            // super contrived override example!
            surface() {
                return superSurface() * 1
            },
            volume() {
                return self.surface * self.width
            }
        }
    }))

// no inheritance, but, union types and code reuse
const Shape = types.union(Box, Square)

And another one:

const CreationLogger = types.model().actions(self => ({
    afterCreate() {
        console.log("Instantiated " + getType(self).name)
    }
}))

const BaseSquare = types
    .model({
        width: types.number
    })
    .views(self => ({
        surface() {
            return self.width * self.width
        }
    }))

export const LoggingSquare = types
    .compose(
        // combine a simple square model...
        BaseSquare,
        // ... with the logger type
        CreationLogger
    )
    // ..and give it a nice name
    .named("LoggingSquare")

Applying that to your needs: Square and Box can be in different files, where Box.js imports Square from Square.js in the first example.

Same exact technique can be applied to the second example.

Mesenchyme answered 8/1, 2019 at 2:42 Comment(3)
That's a great example of model composition, thanks @elektronik :) Model composition is a great feature of MST, but sometimes you just want to split a model across multiple files for easier organization.Asbestosis
@LuisHerranz, the thing is: why would you split the model definition - and not the model itself? Does that give you any advantage? MST allows you to express your intent in higher level primitives. Why not leverage that?Mesenchyme
You may want to split model definition just for the purpose of organizing your files differently. Maybe you want to use one file only for actions, another only for views and so on. You don't always want to keep the constraint of "1 model" = "1 file" and thankfully that's very easy with MST :)Asbestosis

© 2022 - 2024 — McMap. All rights reserved.