mobx-state-tree icon indicating copy to clipboard operation
mobx-state-tree copied to clipboard

Error while converting Model to Model

Open garrettg123 opened this issue 5 years ago • 9 comments

Bug report

  • [X] I've checked documentation and searched for existing issues
  • [X] I've made sure my project is based on the latest MST version
  • [x] Fork this code sandbox or another minimal reproduction.

Sandbox link or minimal reproduction code

I'm trying to add a model to an array which is located in a different child of the parent. However, I also want to return the model from the action so that the view can use it as a navigation parameter.

      const model = Model.create(data)
      getParent(self).addToOtherChild(model)
      return model

Describe the expected behavior

Model should be added to array in other branch.

Describe the observed behavior

[mobx-state-tree] Error while converting <Model@<root>(id: 42> to `Model`:
value of type Model: <Model@<root>(id: 42)> is not assignable to type: `Model`, expected an instance of `Model` or a snapshot like `snapshotProcessor({ ... })` instead. (Note that a snapshot of the provided value is compatible with the targeted type)

I assume this means I can only add the model in its snapshot format. Why can't I add the model when the array has type types.array(Model)?

garrettg123 avatar Jan 21 '20 06:01 garrettg123

You should be able to add model instances to the array as well as snapshots. Make sure you don't accidentally have two different kind of models. Otherwise, share some more code or a small reproduction

mweststrate avatar Jan 21 '20 09:01 mweststrate

Ok, here's some more code. First, the error:

    [mobx-state-tree] Error while converting <ExistingJournalEntry2@<root>(id: 1)> to `ExistingJournalEntry2`:

        value of type ExistingJournalEntry2: <ExistingJournalEntry2@<root>(id: 1)> is not assignable to type: `ExistingJournalEntry2`, expected an instance of `ExistingJournalEntry2` or a snapshot like `snapshotProcessor({ id: identifierNumber; analysis: <any immutable value>?; startDate: (Date | undefined?); endDate: (Date | undefined?); pauseTimes: ({ startDate: Date; textIndex: (number | undefined?); endDate: (Date | undefined?) }[] | undefined?); text: (string | undefined?); geocoderLocationData: (<any immutable value> | undefined?); locationData: (<any immutable value> | undefined?); weatherData: (<any immutable value> | undefined?); timezone: (string | undefined?) })` instead. (Note that a snapshot of the provided value is compatible with the targeted type)

What's weird to me is it says Note that a snapshot of the provided value is compatible with the targeted type, which makes sense, because when I pass the raw object instead of the model to the list, it works.

Now, the models NewJournalEntry and ExistingJournalEntry, both composed from BaseJournalEntry. NewJournalEntry.save() action is where it creates an ExistingJournalEntry and fails:

const BaseJournalEntry = types
  .model('BaseJournalEntry', {
    startDate: types.maybe(types.Date),
    endDate: types.maybe(types.Date),
    pauseTimes: types.maybe(
      types.array(
        types.model('PauseTime', {
          startDate: types.Date,
          textIndex: types.maybe(types.number),
          endDate: types.maybe(types.Date),
        })
      )
    ),
    text: types.maybe(types.string),
    geocoderLocationData: types.maybe(types.frozen()),
    locationData: types.maybe(types.frozen()),
    weatherData: types.maybe(types.frozen()),
    timezone: types.maybe(types.string),
  })

const NewJournalEntry = types.compose(
  types.model('NewJournalEntry', {}).actions(self => ({
    // This is the function that fails
    save: flow(function* save() {
      // ...
      console.log('existingJournalEntryData', existingJournalEntryData)
      // This is the line that fails
      const existingJournalEntry = ExistingJournalEntry.create(
        existingJournalEntryData
      )
      state.journalEntryList.items.unshift(existingJournalEntry)
      // ...
    }),
  })),
  BaseJournalEntry,
  Requestable,
  Cacheable({ key: 'newJournalEntry', isSelfLoadable: true })
)

const ExistingJournalEntry = types.snapshotProcessor(
  types
    .compose(
      types
        .model('ExistingJournalEntry1', {
          id: types.identifierNumber,
          analysis: types.optional(types.frozen(), {}),
        }),
      BaseJournalEntry,
      Requestable
    )
    .named('ExistingJournalEntry2'),
  {
    preProcessor(snapshot) {
      const processed = {
        ..._.omitBy(snapshot, _.isNil),
        startDate: snapshot.startDate
          ? new Date(snapshot.startDate)
          : undefined,
        endDate: snapshot.endDate ? new Date(snapshot.endDate) : undefined,
        pauseTimes: snapshot.pauseTimes
          ? snapshot.pauseTimes
              .map(pauseTime => ({
                ...pauseTime,
                startDate: new Date(pauseTime.startDate),
                endDate: pauseTime.endDate
                  ? new Date(pauseTime.endDate)
                  : undefined,
              }))
              .sort((pauseTime1, pauseTime2) =>
                pauseTime1.textIndex > pauseTime2.textIndex ? 1 : -1
              )
          : undefined,
      }
      console.log('Running preprocessor', snapshot, processed)
      return processed
    },
  }
)

Here is what is printed:

  console.log models/JournalEntry.js:425
    existingJournalEntryData {
      id: 1,
      text: 'This is sample text.',
      startDate: 2020-04-06T16:10:20.600Z,
      endDate: 2020-04-06T16:10:20.608Z,
      locationData: undefined,
      geocoderLocationData: undefined,
      weatherData: undefined,
      pauseTimes: undefined
    }

  console.log models/JournalEntry.js:937
    Running preprocessor {
      id: 1,
      text: 'This is sample text.',
      startDate: 2020-04-06T16:10:20.600Z,
      endDate: 2020-04-06T16:10:20.608Z,
      locationData: undefined,
      geocoderLocationData: undefined,
      weatherData: undefined,
      pauseTimes: undefined
    } {
      id: 1,
      text: 'This is sample text.',
      startDate: 2020-04-06T16:10:20.600Z,
      endDate: 2020-04-06T16:10:20.608Z,
      pauseTimes: undefined
    }

  console.log models/JournalEntry.js:937
    Running preprocessor {
      id: 1,
      text: 'This is sample text.',
      startDate: 2020-04-06T16:10:20.600Z,
      endDate: 2020-04-06T16:10:20.608Z,
      locationData: undefined,
      geocoderLocationData: undefined,
      weatherData: undefined,
      pauseTimes: undefined
    } {
      id: 1,
      text: 'This is sample text.',
      startDate: 2020-04-06T16:10:20.600Z,
      endDate: 2020-04-06T16:10:20.608Z,
      pauseTimes: undefined
    }

  console.log models/JournalEntry.js:937
    Running preprocessor {
      id: 1,
      analysis: {},
      startDate: 1586189420600,
      endDate: 1586189420608,
      pauseTimes: undefined,
      text: 'This is sample text.',
      geocoderLocationData: undefined,
      locationData: undefined,
      weatherData: undefined,
      timezone: undefined
    } {
      id: 1,
      analysis: {},
      startDate: 2020-04-06T16:10:20.600Z,
      endDate: 2020-04-06T16:10:20.608Z,
      text: 'This is sample text.',
      pauseTimes: undefined
    }

As you can see, the data looks fine, and the error message even says the snapshot would work. So I'm unsure as to why creating the model first doesn't work.

One thing that's also confusing to me is that the preprocessor runs 3 times. I'm not sure why. Once for ExistingJournalEntry.create, then maybe a second time for state.journalEntryList.items.unshift(existingJournalEntry), which would be unnecessary anyways.

Hopefully you can figure this out! It's still usable when I pass the pojo, but when I create the model myself it fails.

garrettg123 avatar Apr 06 '20 16:04 garrettg123

This appears to be an issue with models wrapped with types.snapshotProcessor @mweststrate

garrettg123 avatar Apr 19 '20 22:04 garrettg123

Please not that MST doesn't have classical OO inheritance, this means that instances of different types are never compatible with each other. If you want multiple types to be assignable to the same thing, you have to use union types to be able to mix and match them. See https://mobx-state-tree.js.org/tips/inheritance. I'm not sure that is the actual problem, but your code misses some relevant info where some types are defined. Could you instead create a minimal sandbox to show where it goes wrong?

mweststrate avatar Apr 20 '20 09:04 mweststrate

I will try to reproduce if I have time, but I'm pretty sure it's just with models wrapped with snapshotProcessor.

garrettg123 avatar Apr 25 '20 11:04 garrettg123

@mweststrate here is a basic example. Maybe your solution to this will solve my more complex implementation:

https://codesandbox.io/s/mobx-state-tree-todolist-mz01w?file=/index.js

garrettg123 avatar Jul 07 '20 00:07 garrettg123

This ticket is related #1506

...this means that instances of different types are never compatible with each other...

Even when the instances are of the same type this error occurs due to the validation check, because of the SnapshotProcessor wrapping

The SnapshotProcessor can either override the validate method where this.isAssignableFrom(instance) is used and pass the expected value there or override isAssignableFrom check to pass for instances matching the subtype.

The latter allows to use a "plain" value in place of the same type but with a snapshot processor and vice versa and also fixes the mentioned issues

kidroca avatar Sep 14 '20 18:09 kidroca

@kidroca is there a workaround for this? beside for #1581

AviKKi avatar Dec 12 '20 19:12 AviKKi

The workaround is to assign a JSON value (snapshot) instead of an instance

Code like this:

const instance = ComplexType.create(data);
self.someComplexType = instance;
// or
self.someCollection.put(instance);

Should become:

const instance = ComplexType.create(data);
const snap = getSnapshot(instance);
self.someComplexType = snap;
// or
self.someCollection.put(snap);

If your snapshot processor defines both pre and post processing and pre(input: SomeType) matches post(...): SomeType you're fine. You don't need anything more. Otherwise look below

But your snapshot processor will have to handle 2 kinds of input

export const ComplexType = types.snapshotProcessor(MyType, {
  preProcessor(sn: ICustomInput|IPlainSnap = {}) {
    if (isPlainSnap(sn)) return sn; // this added

    return processCustomInput(sn); // this is the original code of the processor
  }
})

const isPlainSnap = (it: any): it is IPlainSnap => {
  return Boolean(it.someCheckThatConfirmsInterfaceMatch);
};

If there is a postProcessor handling, and it returns anything other than the original structure (ICustomInput in this example) it becomes a bit more complicated than that - you'll have to handle IPostProcessedSnap instead of IPlainSnap as an input for the preProcessor

Because this will be the post processed snap:

const postProcessedSnap = getSnapshot(instance);
self.someComplexType = postProcessedSnap;

kidroca avatar Dec 13 '20 21:12 kidroca

Hey folks, looks like this issue was resolved. Underlying issue is the snapshot processing in MST, and you can work around it by providing JSON instead of instances in some cases.

I think this is pretty much intended behavior with a reasonable path to resolution. I'm going to close this out, but if anyone wants to give some more feedback on changing the behavior, I would definitely encourage hopping in the discussions.

coolsoftwaretyler avatar Jun 30 '23 01:06 coolsoftwaretyler