Skip to content
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

avoid updates when value did not change (with custom equality function) #25

Open
pothos-dev opened this issue Jun 3, 2022 · 4 comments

Comments

@pothos-dev
Copy link

pothos-dev commented Jun 3, 2022

I have a graph of observables, that originate from a rapidly changing one (a redux store that we want to migrate away from).

I want derived observables to only propagate updates if their value actually changed.
I want to use a custom equality function to determine if something changed (e.g. structual equality over reference equality).

I know that I can compare current and last value inside subscribe, but most of my observables are not directly subscribed, but are used as inputs to other observables, which are unnessecarily recomputed.

I propose an additional, optional parameter isEqual(next: T, current: T): boolean, to .select() and .compute(), after the lambda function, where you can pass in an equality function that is evaluated on every update after the initialitation of the observable. If it returns true, the observable is not updated with the computed value.

import { isEqual } from "lodash"

const signalSpeed: Observable<Speed> = reduxStore.select(store => ({
  pxPerMm: store.config?.getSettings("pxPerMm") ?? 0,
  mmPerSec: store.config?.getSettings("signalSpeed") ?? 0,
}), isEqual)
@lubieowoce
Copy link

lubieowoce commented Jun 4, 2022

Hey, looks like we can work around this "in userspace", using the previous value we get in subscribe!

import { observable, Observable } from 'micro-observables';

export type Equality<T> = (a: T, b: T) => boolean;

/**
* Takes an observable and produces a new one, which updates
* only if equals returns false.
* Example usage:
*  const obs2 = sameIf(isEqual)(obs1)
**/
export function sameIf<T>(equals: Equality<T>) {
return (original: Observable<T>): Observable<T> => {
  const derived = observable(original.get());
  original.subscribe((newVal: T, oldVal: T) => {
    if (!equals(newVal, oldVal)) {
      derived.set(newVal);
    }
  });
  return derived;
};
}

// or, if you prefer it as a constructor
export function observableMemo<T>(value: T, equals: Equality<T>): Observable<T> {
return sameIf(equals)(observable(value));
}

(sorry about the indents, github seems to mess them up no matter what i do)

codesandbox with some react code to check it out:
https://codesandbox.io/s/little-morning-jcq03u?file=/src/App.tsx

You'd still need to wrap everything with sameIf(myEquals)(...), but that's better than nothing i guess... or perhaps do something like this if you wanna save yourself some typing:

const selectIf = (equals) => (obs, selector) => sameIf(equals)(obs.select(selector))

Some alternative names: memoIf, keepIf, updateUnless. The original one was memoIf, but i feel like "memo" is a bit too general

@pothos-dev
Copy link
Author

pothos-dev commented Jun 4, 2022

This is something I am doing right now, but I am afraid that this solution is prone to memory leaks.

I assume that a regularly derived observable via .select() can be garbage collected when the last reference to it is deleted.

Using the solution outlined by lubieowoce, by explicitly creating a subscription on the source observable and referencing the target observable, we bind the lifetime of the target observable to the source observable, and cannot simply "let it go".

I noticed that there is an undocumented Plugin architecture in the library that supports stuff like onAttach etc.. so I think we might be able to make it memory safe using this, but since it's undocumented, kinda hard to say.

@simontreny
Copy link
Collaborator

simontreny commented Jun 6, 2022

This is something I am doing right now, but I am afraid that this solution is prone to memory leaks.

I assume that a regularly derived observable via .select() can be garbage collected when the last reference to it is deleted.

Yes, you're right, deriving observables using subscribe() will cause memory leaks as the source observables won't be garbage collected unless the unsubscribe function returned by subscribe() is explicitly called.

I agree that equality function is definitely something that should be built-in directly in micro-observables. We actually already added support for it in the next major version of the library (that is not released yet). I've quickly tried to backport it into the 1.x version but unfortunately, this is not straightforward. The next version should hopefully be released in the next few weeks.

I noticed that there is an undocumented Plugin architecture in the library that supports stuff like onAttach etc.. so I think we might be able to make it memory safe using this, but since it's undocumented, kinda hard to say.

This API was an experiment to add support for persistence and dev tools to micro-observables but we will drop it in 2.x in favor of another mechanism. Anyway I don't think it can be used to achieve what you want here.

@pkieltyka
Copy link

hey @simontreny any updates on this one? would be cool to see 2.x and also if you've considered react v18 concurrent mode + useSyncExternalStore ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants