-
Notifications
You must be signed in to change notification settings - Fork 641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Rethinking references API #1282
Comments
Initial reaction: like! Just a random idea, didn't think it through, what if we would keep things as is, but that |
What about The one thing I don't quite like though is that the typing would be still kind of hard to do... So something like this then?
The other thing I don't like is that you need to make sure to use the parent object when accessing the child or else it won't work, whereas |
Hey, what about external (not-from-tree) refs? |
If this fixes #1284, it would dramatically simplify some complex models I have. Are we currently awaiting a decision, or for someone to volunteer to implement? |
I think it would be great if @xaviergonz suggestion was implemented:
|
@Amareis those can already be implemented using custom refs. |
While impelmenting stuff described in #1355 I thought of adding special types Just a basic API overview: const myArrayType = types.array(SomeSubType); // it would be impossible to inline this into model
const modelType = types.model({
items: myArrayType
itemReferences: types.arrayReference(myArrayType , mode: 'standalone' | 'safe' | 'sync', custom?: ReferenceOptionsGetSet)
}) The
None of the variants assume 'two-way-sync' (i.e. adding to The main benefit is that we could remove fair amount of logic brought by New implementation birdview:
Steps 3 and 4 are needed, because any part of the tree could be 'hidden' inside To support custom references across trees const myArrayType = types.array(SomeSubType); // it would be impossible to inline this into model
const sourceModelType = types.model({ items: myArrayType })
const sourceModel = modelType.create(...)
const referrerModelType = types.model({
refs: types.arrayReference(() => sourceModel.items, 'sync')
})
const referrerModel = referrerModelType.create();
referrerModel.refs // is fully syncronized with sourceModel.items |
Even simpler API: |
I really love the idea @k-g-a, especially having designated types for collections of references could greatly simplify stuff. I think synced deletes should be the default (and non configurable ?) Overal I think we might have designed references too transparent, I think in future improvements we should maintain explicit reference objects with an explicit api, so that we can introduce all the utility methods discussed above and also introduce lazy resolving / loading of references. |
What prevents us to implement references as an external library? |
I'm currently working on a similar implementation to the proposal by @xaviergonz. It's based on a real model. Why do I need a real model? Because then I can use For example, here is the case. const ApplicationModel = types.model("Application", {
id: types.identifierNumber
});
const PostModel = types.model("Post", {
id: types.identifierNumber,
application: types.reference(ApplicationModel)
});
const EntitiesModel = types.model("EntitiesModel", {
applications: types.map(ApplicationModel),
posts: types.map(PostModel)
});
const RootModel = types.model("Root", {
posts: types.array(types.reference(PostModel)),
entities: types.optional(EntitiesModel, {})
}); So now we can create our const root = RootModel.create({
posts: [1],
entities: {
posts: {
1: {
id: 1,
application: 1
}
},
applications: {
1: {
id: 1
}
}
}
}); The whole thing works great with the current reference implementation. But there is one thing which gets tricky. Since Application can basically be a part of the post, we need some access to it from the application, too. But when we do The only implementation I could think about - to use a real model and custom registry for all the references to some type. // first of all we need some place to store all our references
const RefsRegistry = new Map<IAnyComplexType, Map<string, IStateTreeNode>>();
export function createEntityRef<T extends IAnyComplexType>(
entityName: string,
Model: T
) {
// find the registry based on the model type
let currentRegistry = RefsRegistry.get(Model);
if (!currentRegistry) {
currentRegistry = new Map();
RefsRegistry.set(Model, currentRegistry);
}
const refModel = types
.model(`${Model.name}Ref`, {
// or can be a number
id: types.number
})
.volatile(self => ({
// we need internal ref to store our reference
_refId: `${Model.name}Ref-${uuid()}`
}))
.views((self: any) => ({
get current() {
const entities = getRoot<any>(self).entities;
const entityModel = entities[entityName];
// currently I resolve it from the entityModel but it can be stored anywhere
return entityModel.get(self.id) as Instance<T>;
}
}))
.actions(self => ({
// we store our reference as soon as this model created
afterCreate() {
currentRegistry!.set(self._refId, self);
}
}));
return refModel;
} With that fabric now we can replace our const applicationRef = createEntityRef('applications', ApplicationModel);
const PostModel = types.model("Post", {
id: types.identifierNumber,
application: applicationRef
}); In order to make it work out of the box, we need to add snapshotProcessor to the ref model so it can receive number or string as a snapshot and convert it to Now we can use this function inside of ApplicationModel to get the parent of type Post of the ref (or parents): export function getReferenceParentOfType<T extends IAnyModelType>(
model: IStateTreeNode,
parentType: T
): T["Type"] | undefined {
const type = getType(model);
const registry = RefsRegistry.get(type);
if (!registry) {
return undefined;
}
for (let value of registry.values()) {
try {
return getParentOfType<T>(value, parentType);
} catch {}
}
return undefined;
} And still, this works great. But there is another problem - mobx-state-tree is lazy by default. That means this block will be executed only when I actually access this ref. afterCreate() {
currentRegistry!.set(self._refId, self);
}, This is probably OK for most of the cases, but there is another cool one. The other really useful case is to be able to find all the references to the specific model and be able to work with those references (for example, to remove from all the lists): export function resolveModelReferences<T extends IAnyModelType>(
type: T
): undefined | any[] {
const registry = RefsRegistry.get(type);
if (!registry) {
return undefined;
}
return [...registry.values()];
} But this will resolve all the accessed reference models because rest of them won't be stored inside the registry of the specific model type so again this block won't be executed until I access the reference model: afterCreate() {
currentRegistry!.set(self._refId, self);
}, Any ideas on how to work around this? |
Right now refs are kind of hard to grasp for users, since they are actually ids that hide a resolution logic to objects.
Also they exhibit the following problems:
The proposal is a new API for using refs, something like this:
Technically it could be a non breaking change by adding a new ref type (or actually two with safeReference), but that'd mean we'd end up with 4 possible ref types
Maybe something like types.ref / types.safeRef
Or maybe something best left for a major version?
The text was updated successfully, but these errors were encountered: