Mobx State Tree reference type and Typescript
Asked Answered
W

2

8

I'm using mobx-state-tree with Typescript in a React application. And, I'm having an issue with Typescript where it complains about the type of the mobx type types.safeReference. It looks like the type of safeReference in the model definition is different from its type when you use .create() to actually create an instance of the model. In my code, selectedProduct's type is converted to string | number | undefined | null in productStore, but in the model definition is IStateTreeNode<...> | undefined | null and that's why I get an error in my root store. How can I fix that?

Here's my Product store:

import { types } from "mobx-state-tree";

const Product = types.model("Product", {
   id: types.identifier,
   name: types.string
})

const ProductStore = types
  .model("ProductStore", {
    products: types.array(Product),
    selectedProduct: types.safeReference(Product),
  })
  .actions((self) => ({
      // actions here
  }));

export const productStore = ProductStore.create({
  products: [],
  selectedProduct: undefined // the type here is different from the type in the actual model
});

And, here's my root store:

import { types } from "mobx-state-tree";
import ProductStore, { productStore } from "./product-store";

const RootStore = types.model('RootStore', {    
  productStore: ProductStore 
})

export const rootStore = RootStore.create({
    productStore: productStore // Here is where I get the typescript error.
});

UPDATE:

Another way of reproducing this issue is by trying to create a custom reference. The getter will complain about undefined not being assignable to type {...}.

const ProductByIdReference = types.maybeNull(
  types.reference(Product, {
      get(id: number, parent: Instance<typeof ProductStore>) {
          return parent.products.find(p => p.id === id) || undefined
      },
      set(value: Instance<typeof Product>) {
          return value.id
      }
  })
)
Winograd answered 29/1, 2021 at 16:51 Comment(5)
I'm not sure if this is an oversight or the intended behaviour, very interesting. Instead of exporting a ProductStore singleton, could you not let the RootStore create it for you? I.e. export const rootStore = RootStore.create({ productStore: { products: [], selectedProduct: undefined } });Weekday
No, it has to be singleton.Winograd
Is this a case where everything works fine and it's just a TS error? It seems like RootStore.create is looking for a raw value rather than a created store. If you pass it the initial state of productStore rather than the store itself then it works fine. Not sure if this is at all useful but this answer shows a very different way to combine stores: https://mcmap.net/q/1470770/-how-to-split-mobx-state-tree-models-across-multiple-filesCitric
There's a bunch of utility types at play github.com/mobxjs/mobx-state-tree/blob/… But the lowest down message in the error chain is that you have an IMSTArray github.com/mobxjs/mobx-state-tree/blob/… and it expects an actual array. It complains that IMSTArray does not have all of the methods that an array should.Citric
Thank you @LindaPaiste, and sorry for the delay! I'll give that a try.Winograd
W
5

Generally speaking when you are trying to use a snapshot where an instance is typically used, you use cast. When you are trying to use an instance where a snapshot is typically used, you use castToSnapshot.

export const rootStore = RootStore.create({
    productStore: castToSnapshot(productStore)
});
Weekday answered 23/2, 2021 at 7:34 Comment(0)
I
0

I checked your problem in detail and I wrote a solution, this is how to use mst rootStore concept correctly, there is no need to create instances of your store or object, just @inject it inside component directly:

// Product Store
import { types, Instance } from "mobx-state-tree";

const Product = types.model("Product", {
  id: types.identifier,
  name: types.string
})

const Store = types
  .model("ProductStore", {
    products: types.array(Product),
    selectedProduct: types.safeReference(Product),
  })
  .actions((self) => ({
    // actions here
  }));

type ProductStoreType = typeof Store;
interface ProductStoreTypeInterface extends ProductStoreType {}
export interface ProductStoreInterface extends Instance<ProductStoreTypeInterface> {}
export const ProductStore: ProductStoreTypeInterface = Store;

// Root Store
import { types } from "mobx-state-tree";
import { ProductStore } from "./product-store";

const RootStore = types.model('RootStore', {
  productStore: types.late(() => types.optional(ProductStore, {})),
})

// And now you can create rootStore with empty object
export const rootStore = RootStore.create({});

// Component
import { inject, observer } from 'mobx-react';
import { SettingStoreInterface } from "../settings/SettingStore";
import React from "react";

interface ComponentProps {}

interface InjectedProps extends ComponentProps {
  productStore: ProductStoreInterface;
}

@inject('productStore')
@observer
class SomeComponent extends React.Component<ComponentProps> {
  componentDidMount() {
    const { productStore } = this.props as InjectedProps;
    
    console.log(productStore.products);
  }
}
Intarsia answered 6/2, 2021 at 9:22 Comment(7)
But, you're not creating the Product Store.Winograd
It would be created when you inject it in your componentIntarsia
This won't work. The product store has not been created, so it can't be injected.Winograd
I've whole product built this way, you need at least to test, before you write that it doesn't workIntarsia
I'll give it a try, but regardless, this solution as is won't work for me, as I'm using react hooks. I need to be able to use the stores using useContext.Winograd
I can convert it to work with functional componentsIntarsia
Let me try this. I'll get back to you. Thank you! It may take a couple of days, though.Winograd

© 2022 - 2024 — McMap. All rights reserved.