Mobx-state-tree: Typescript checking fails recognize actions

Created on 18 Jan 2018  Â·  20Comments  Â·  Source: mobxjs/mobx-state-tree

Most helpful comment

@yordis yes this can indeed be a bit confusing in typescript, the type of self is what self was before the action blocks starts, and only after that part finishes, the actions will be added to the type of "self".

There are two simple workaround for that 1) to declare several action blocks:

.actions(self =>({
   loadDetails() { /* stuff */ }
}))
.actions(self => ({
   afterCreate() {
      self.loadDetails()
}))

That works, but is a bit cumbersome and doesn't work if the relation is circular. A better solution is to follow inside the actions deifnitions the module pattern, where are functions are declared in the closer, and in the end just export. So you get this:

.actions(self => {
  function loadDetails() {
    // stuff
   }
   function afterCreate() {
      loadDetails() // (note, no self here, just called directly!)
   }
  // expose functions to outside world (only the ones that are needed outside + the lifecycle hooks
   return { loadDetails, afterCreate }
})

Hope that helps! Feel free to open a PR to clarify that in the readme where you would have expected it. (maybe in the typescript tips section?)

All 20 comments

It's a known limitation of TypeScript, there should be an example in the readme :)

@mattiamanzati I am guessing you are talking about splitting your store? Or declare multiple views functions?

These are actions btw, I am assuming that now shouldn't be the same as views?

@mattiamanzati also in this case is because it is missing the function, in other cases is because I am not passing the function as a property which I wouldn't because it is an action no property.

Which in tha case I assume that I should do

const TodoState = types.model({
        title: types.string
    });

type ITodoStateType = typeof TodoState.Type;
interface ITodoState extends ITodoType {}

const Todo = TodoState
    .actions(self: ITodoState  => ({
        setTitle(v: string) {
            self.title = v
        }
    }))

but at that moment which one should I use as the value of another field Todo or TodoState?

.model({
myTodo: Todo or TodoState // <---- Go goes here
})

All the questions is based on assumptions because the documentation is not very clear for 🔰 beginners how to use the package. Probably that is the root of the issues.

@yordis yes this can indeed be a bit confusing in typescript, the type of self is what self was before the action blocks starts, and only after that part finishes, the actions will be added to the type of "self".

There are two simple workaround for that 1) to declare several action blocks:

.actions(self =>({
   loadDetails() { /* stuff */ }
}))
.actions(self => ({
   afterCreate() {
      self.loadDetails()
}))

That works, but is a bit cumbersome and doesn't work if the relation is circular. A better solution is to follow inside the actions deifnitions the module pattern, where are functions are declared in the closer, and in the end just export. So you get this:

.actions(self => {
  function loadDetails() {
    // stuff
   }
   function afterCreate() {
      loadDetails() // (note, no self here, just called directly!)
   }
  // expose functions to outside world (only the ones that are needed outside + the lifecycle hooks
   return { loadDetails, afterCreate }
})

Hope that helps! Feel free to open a PR to clarify that in the readme where you would have expected it. (maybe in the typescript tips section?)

@mweststrate I would love to know what is the cost of calling many times actions, views?

Trying to figure out what is the best guide for Typescript users in terms of organization of the code.

Also this example

.actions(self => {
  function loadDetails() {
    // stuff
   }
   function afterCreate() {
      loadDetails() // (note, no self here, just called directly!)
   }
  // expose functions to outside world (only the ones that are needed outside + the lifecycle hooks
   return { loadDetails, afterCreate }
})

Fail for me in some of the code for me in the past because expected me to actually do self.FUNCTION because the mutations should happen in the actions. there is some weird situations with it which I will trying to replicate it so we can document how it works and what we should do.

This is a little concern about it

import { flow, getEnv, types } from 'mobx-state-tree'

const mock = (value: any ) => {
  return new Promise((resolve) => {
    resolve(value)
  })
}

export const UserStore = types
  .model('User', {
    loading: types.optional(types.boolean, true),
    uid: types.identifier()
  })
  .views((self) => ({
    get httpClient() {
      return getEnv(self).httpClient.get()
    }
  }))
  .actions((self) => {
    const loadDetails = function* (where: string) {
      console.log('Started: ', where)

      yield mock('123')

      console.log('Almost there: ', where)

      yield mock('456')

      self.loading = false

      console.log('Finished: ', where)

    }

    return {
      loadDetails: flow(loadDetails),
      afterCreate() {
        self.loadDetails('flow')

        // VS

        const gen = loadDetails('manually')
        console.log(gen.next().value)
      }
    }
  })


const store = UserStore.create({
  uid: "123"
})

Output

Started:  flow
Started:  manually
Object {}
Almost there:  flow
Finished:  flow

I think it is worth to document about it unless you have some workaround for this issue.

@mattiamanzati Confirmed that I can't do what you suggested

This code fails on execution

.actions((self) => {
    const loadDetails = async () => {
      const response = await UserService(self.httpClient).getUser(self.uid)
      const userDetails = response.data[0]
      self.details = userDetails
    }

    return {
      afterCreate() {
        loadDetails()
      }
    }
  })

Error

: [mobx-state-tree] Cannot modify 'User@/currentUser/user(id: qCJsXVWRqHccTORjJa2OlrW5ukx1)', the object is protected and can only be modified by using an action.
  1. don't worry about the action call performance itself, it is O(1)
  2. it will be part of the afterCreate action, so you can still mutate fine.

For the flows, pull the flow up, so define it as:

const loadDetails = flow(function* (){ /* stuff */ })

return { loadDetails }

It is flow that promisifies the generator, attaches middleware etc

@mweststrate still the situation about pulling out to the function it keep giving me the error about updating the store outside of the an action for some reason.

Are you on the latest version of MST?

"mobx-state-tree": "^1.3.1",

@mweststrate hah here is the catch.

If I use async instead of generator it will give me the error for sure. Maybe because I am not wrapping it with flow ...

Which is weird because everything started from an action (meaning that afterCreate is an action), but internals I guess.

oh yes, you have to wrap it with flow

@mweststrate better to document that one because I got that when I create an async action I have to wrap it but this is not technically an action but a function being call inside an action (we can export it as an action but that is an effect)

So all the async operations most be wrap with flow if we want to do any mutation.

Like here https://github.com/mobxjs/mobx-state-tree/blob/master/docs/async-actions.md#using-generators ;-)?

@mweststrate I saw that section but what people could miss from it is that in anywhere in your code you should use flow

If I would create an action that requires some async I would use it because I read the documentation but because this was a function call inside another action then it wasn't clear. The documentation talks about the advantage and usage of generators but do not explain this situation that well, unless you run into this issues I am facing because of Typescript forcing me to change the code style to be everything a closures, you wouldn't extract those functions and you would leave it as direct actions

Feel free to create PR to make things more clear 😊

Op vr 19 jan. 2018 12:27 schreef Yordis Prieto notifications@github.com:

@mweststrate https://github.com/mweststrate I saw that section but what
people could miss from it is that in anywhere in your code you should use
flow

If I would create an action that requires some async I would use it
because I read the documentation but because this was a function call
inside another action then it wasn't clear. The documentation talks about
the advantage and usage of generators but do not explain this situation
that well, unless you run into this issues I am facing because of
Typescript forcing me to change the code style to be everything closures.

—
You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx-state-tree/issues/610#issuecomment-358939942,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABvGhIancYfuZPy_iAUrHfyyfh13Q69Xks5tMHwJgaJpZM4Rjav1
.

screen shot 2018-01-22 at 2 55 52 pm

@mweststrate look at this code

const ApplicationStore = types
  .model('ApplicationStore', {
    currentUser: types.maybe(CurrentUserStore)
  })

const CurrentUserStore = types
  .model('CurrentUser', {
    token: types.string,
    user: UserStore
  })

const UserStore = types
  .model('User', {
    details: types.maybe(UserDetailsStore),
    uid: types.identifier(types.string)
  })

Notice that my User store have .maybe type so it shouldn't fail when I do not have values for it.

Readme section has been updated. Thanks for the PR @yordis

@mweststrate Should the forward declaration approach work with Promises? When I run this code it tells me I can't modify the store.

const MyStore = types
  .model("MyStore", {
    isLoading: types.optional(types.boolean, false)
  })
  .actions(self => {
    function handleSignup() {
      self.isLoading = true;

      return fakeSignup().then(handleSignupSuccess);
    }

    function handleSignupSuccess() {
      self.isLoading = false;
    }

    return {
      handleSignup,
      handleSignupSuccess
    }
  })
Was this page helpful?
0 / 5 - 0 ratings