mobxjs / mobx-state-tree

Full-featured reactive state management without the boilerplate

Home Page:https://mobx-state-tree.js.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Storing Dynamic Volatile Metadata on MST Nodes

BrianHung opened this issue · comments

I'm trying to use ObservableGroupMap (OGM) from mobx-utils to optimize map-to-arrays (mobxjs/mobx-utils#314).

OGM relies on using symbols to store metadata on nodes: https://github.com/mobxjs/mobx-utils/blob/1a53977f103c5f177dae0ff2a1ac348deb7cf2a2/src/ObservableGroupMap.ts#L195

When I delete a MST node, we run into two errors. First is from _getAssertAliveError

private _getAssertAliveError(context: AssertAliveContext): string {
where escapeJSONPath fails because it doesn't support a subpath which is a Symbol.

TypeError: path.indexOf is not a function

Second error is from

if (!this.isRunningAction() && this.isProtected) {
throw fail(
`Cannot modify '${this}', the object is protected and can only be modified by using an action.`
)
}
}

Is there a way to allow enumerable: false properties like Symbols to be dynamically set without affecting serialization.

Hey @BrianHung! I'm sorry this went so long without a response. Unfortunately I don't have a great answer for you at the moment, but I was wondering if you had found a way to resolve the issue in case other folks run into the same problem.

If you haven't, I am happy to put some time into trying to help out if it's still an issue on your end.

@coolsoftwaretyler

I forked observable group map to store the metadata on the mobx atom backing a MST node (the dehance, enhance thing), so that it wouldn't run this issue. But it's a leaky abstraction.

Thanks, @BrianHung! Glad there was at least a path forward for you, and perhaps we can use that idea to guide future work on something like this if it gets prioritized. Appreciate the follow-up!

@BrianHung - is this something you would want to bring to MST itself? I don't fully understand the problem/solution here, but I'd love to discuss it further if you found this to be helpful and improve your experience using MST.

If you're uninterested, or if I don't hear back from you in about two weeks, I'll probably close out the issue.

is this something you would want to bring to MST itself? I don't fully understand the problem/solution here, but I'd love to discuss it further if you found this to be helpful and improve your experience using MST.

The context of the problem is that I have a normalized map of cells and rows, and want to define a view that gets all the cells that belong to a row.

const cell = t.model("cell", {
  id: t.refinement(t.identifier, (id: string) => isUUID(id)),
  value: t.maybeNull(t.union(t.string, t.number)),
  row: t.reference(
    t.late((): IAnyModelType => Row),
    { onInvalidated: deleteSelf }
  ),
})

const row = t.model("row", {
  id: t.refinement(t.identifier, (id: string) => isUUID(id)),
  height: t.maybeNull(t.number),
  order: t.string,
}).views(self => ({
  get cells() {
    const state = getRoot(self);
    const cells = Array.from(cells.values()).filter(cell => cell.row === self);
    return cells.sort((x, y) => (x.col.order < y.col.order ? -1 : 1));
  }
})

This approach turns out to be inefficient, since any time a cell is added to the global map of cells, all the rows cells view recomputes even if the new cell doesn't belong to that row.

To optimize this, I used observable group map (OGM) from mobx-utils https://github.com/mobxjs/mobx-utils/blob/master/src/ObservableGroupMap.ts which caches the array of cells if the new cell doesn't match the group / row id.

The problem itself is that OGM uses a symbol to attach metadata on each MST node https://github.com/mobxjs/mobx-utils/blob/b7dcd7d0f97a5f0e1daf96cc44e88d434cd470d0/src/ObservableGroupMap.ts#L195-L199; in this case, its per cell.

The solution I would be looking for with MST, is for MST to support storing metadata on a MST node that is not persisted. Alternatively, how could one rewrite OGM to work with MST and not store metadata directly on the MST node.

Not familiar enough with how MST uses mobx to answer that myself; although OGM works with vanilla mobx, so not sure why it can't work with MST.

Thanks for the extra context!

I'm really just getting my head around the codebase, so I don't have a great starting point, myself. But I think it would be worth some initial exploration.

Would you want to put together a spike branch, maybe where we start with a failing test that demonstrates the desired behavior, and how it doesn't work currently? From there, we could troubleshoot what changes we need to make to the library to support, and then we could determine if it's a good fit to resolve.

Jamon and I spoke last week about the direction of MST, and our goal is to prioritize stability over all else. So if this can be done without breaking changes (perhaps some kind of additional utility or opt-in flag), I think there could be a path forward. I would just need you to take the lead on it.

From there, we could troubleshoot what changes we need to make to the library to support

Is it possible to get guidance from Jamon on whether the mobx node backing a MST node is a valid object to put metadata? If yes, then I think this issue can be closed without needing a test case.

Gotcha. I'll see if I can flag him down and get an answer. In the meantime, can you clarify what you mean by valid object for metadata? Do you just mean it won't break MST internals if you do something like this, or something else?

Is it possible to get guidance from Jamon on whether the mobx node backing a MST node is a valid object to put metadata?

I'd probably use the $treenode property object and attach any metadata you need there. Would that work for your needs?

// @ts-ignore
myobject.$treenode.mymetadata = {
  // whatever you want here
}

https://codesandbox.io/s/stupefied-pascal-jvqjpf?file=/src/App.tsx

Do you just mean it won't break MST internals if you do something like this, or something else?

Yes; either won't break MST internals or result in patches being created -- think that's the one area where MST differs from mobx, and would prevent normal mobx utils being used on MST.

I'd probably use the $treenode property object and attach any metadata you need there.

Would using addHiddenWritableProp also work?

export function addHiddenFinalProp(object: any, propName: string, value: any) {
defineProperty(object, propName, {
enumerable: false,
writable: false,
configurable: true,
value
})
}
/**
* @internal
* @hidden
*/
export function addHiddenWritableProp(object: any, propName: string, value: any) {

Noticed that utility when look at how $treenode gets attached itself.

I'm not very familiar with that part of the code base so ... give it a shot, see what happens! 😅

Closing this issue since $treenode works at the minimum; thanks!