I'm working on a query synchronization library that aims to return reactive objects for use in Vue and React components. Right now, I'm trying to integrate our library's live querysets with Vue, using this basic approach:
```javascript
export function QuerySetAdaptor(liveQuerySet, reactivityFn = reactive) {
const wrapper = reactivityFn([...liveQuerySet]);
const renderHandler = (eventData) => {
wrapper.splice(0, wrapper.length);
wrapper.push(...liveQuerySet);
};
querysetEventEmitter.on(eventName, renderHandler);
return wrapper;
}
```
Our library wraps the data internally when returning it:
```javascript
const users = myLib.getUsers(); // This already returns a reactive wrapper
```
**The aim:** Keep `users` in sync with our library's internal state while ensuring that it can be garbage collected when unused (e.g., during re-renders or when components unmount).
**The issue:** Reactivity systems in Vue and React prevent the `wrapper` from being garbage collected because:
1. The event listener holds a reference to `wrapper`.
2. Framework internals track `wrapper`, keeping it alive indefinitely.
3. These references aren't cleaned up, causing memory buildup.
**My question:** Is there a way to design libraries that return reactive objects that can:
1. Stay in sync with internal state?
2. Allow for garbage collection when they're not in use?
3. Have a straightforward cleanup mechanism for users?
Or, is it just better to stick with subscribe/unsubscribe APIs instead of returning reactive objects? I'm really looking for insights from library developers who have navigated this across different frameworks.
3 Answers
After testing, I found that with Vue, you can add the following to help with the cleanup:
```javascript
onScopeDispose(() => {
querysetEventEmitter.off(eventName, renderHandler);
})
```
This should help with eliminating references at the right time!
You might want to break the problem down a bit. Start by focusing on Vue alone and get it working there. Once that's set, you can see if similar solutions apply to React. That's usually easier than tackling everything at once!
You might want to consider using WeakRef! This way, you can hold a weak reference to the wrapper. If Vue's reactivity doesn't keep it alive, it'll get cleaned up. Here's a code snippet to illustrate the idea:
```javascript
export function QuerySetAdaptor(liveQuerySet, reactivityFn = reactive) {
const wrapper = reactivityFn([...liveQuerySet]);
let ref = new WeakRef(wrapper)
const renderHandler = (eventData) => {
const wrapper = ref.deref()
if (wrapper) {
wrapper.splice(0, wrapper.length);
wrapper.push(...liveQuerySet);
} else {
querysetEventEmitter.off(eventName, renderHandler)
}
};
querysetEventEmitter.on(eventName, renderHandler);
return wrapper;
}
```
This way, if the `wrapper` goes away, you're also removing the listener, which might just solve your problem!
Thanks for the suggestion! I tried this approach, which I got from Claude, but unfortunately, it didn't work. Vue seems to keep these objects alive regardless.
That sounds like solid advice. Right now I'm stuck with Vue. I can get it to be reactive, but keeping everything synced seems to block the GC on the Vue ref/reactive object.