quarrant/mobx-persist-store

Persisting domain object with a dependency on parent store

ekiwi111 opened this issue · 8 comments

What's the preferred pattern to follow in the case of persisting a store with one to many relationship to its dependent domain objects?

E.g. example store

export class ShopStore {
  shopName: string | null = null;
  products: ObservableMap<Product['id'], Product> = observable.map();

  constructor(rootStore: RootStore) {
    makeAutoObservable(this, { rootStore: false }, { autoBind: true });
    makePersistable(this, {
      name: 'ShopStore',
      properties: ['shopName', 'products'],
      storage: window.localStorage,
    });
  }

 createProduct() {
    const product = new Product(this);
    this.products.set(product.id, product);
    return product;
  }
}

And its domain items:

export class Product {
  shopStore: ShopStore;
  id: string;

  constructor(appStore: ShopStore, id = v4()) {
    makeAutoObservable(this, { shopStore: false }, { autoBind: true });
    this.shopStore = appStore;
    this.id = id;
  }
}

In this example it'd throw the following error when persistence is triggered:

Converting circular structure to JSON
--> starting at object with constructor 'ShopStore'
| property 'products' -> object with constructor 'Array'
| index 2 -> object with constructor 'Array'
| index 1 -> object with constructor 'Product'
--- property 'shopStore' closes the circle

This is similar to #41 and #55 where you are trying to save a Map Object with references to classes. So basically you are doing this:

JSON.stringify(new Map(['productId', new Product(appStore)]);

JSON.stringify is trying to convert all the objects to JSON but Product has a reference to ShopStore and there is a circular dependency (Product > ShopStore > Product > ShopStore etc.). Every time JSON.stringify converts a Product to JSON it finds ShopStore and tries to convert that but then it finds more Product in products which is a property on ShopStore and the cycle never ends.

It is very hard to persist classes and/or Map's because if they are converted to JSON, then how do you rehydrate JSON back to classes and Map's. You would have to give makePersistable a custom storage controller that knows to turn the JSON back to classes and finally within a Map.

As of right now there is not a perfect solution for what you are trying to do. Persisting simple objects and arrays is currently the easiest way because in the end you are working with JSON.

@codeBelt Thank you for your explanation. It's a problem with circular dependencies and architecture. If you want to resolve this just remove circular dependencies.

For example, if you want create relation between to classes you may do it with any primitive identifier

class Shop {
    constructor(public readonly name: string) {}
}

class Product {
    constructor(public readonly id: string, public readonly shopName: string) {}
}

const shops = new Map([["appStore", new Shop("appStore")]]);

const appStoreShop = shops.get("appStore")!;

const product_1 = new Product("1234", appStoreShop.name);
const product_2 = new Product("5678", appStoreShop.name);

const productShop = shops.get(product_1.shopName);

This example uses an architecture without circular dependencies. I hope it helps you =)

Is it possible to exclude a property at the class level from being persisted? e.g. makeAutoObservable(this, { rootStore: false }); as an example in 'mobx'.

It'd be great if there'd be a similar functionality in mobx-persist-store, e.g. in Product:

// something like
persistableConfig(this, {
    exclude: ['appStore']
});

Nope, but I’ll think about it

Is there a way to perform a dynamic check on persistance? e.g. in isPersisting?

isPersisting checks only top-level objects, not each nested level. We didn’t do this kind of work because it may be a really hard work.

For example, if I have a nested array with nested objects there are each object has a nested array I need to loop each array to find which property I want to exclude before persisting…

Anyway, I think your case is a good example what we couldn’t do with this library, because it will lead to low performance.

I know an approach how do it better, but not in this library. I need to think about it moreeeee