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

The same object being proxied has different references in scope of the same observable #3917

Open
andres-kovalev opened this issue Sep 4, 2024 · 3 comments

Comments

@andres-kovalev
Copy link

When I assign the same const object as a value of the same observable - I get different reference every time:

store.reactiveField = constObject;
const cache = store.reactiveField;
store.reactiveField = constObject; // even tho we assign the same object, we got different reference
console.log(cache === store.reactiveField); // false
...

I understand store.reactiveField and constObject are different objects (the first one is a proxy for the second one), but it still looks counterintuitive and error prone to get a new reference every time. For instance, when this proxy is used as a dependency for a react hook:

useEffect(() => {
  // this effect is triggered every time I do `store.reactiveField = constObject`
}, [store.reactiveField]);

Is it possible to preserve the reference in a case when the actual proxy target has the same reference?

If there is a reason for such behavior, is there any workaround for that? At the moment the only idea I have is to keep the copy of the original (non-proxied) value and perform deep assign manually:

// without safety checks for simplicity
function deepAssign(reactive, original, value) {
  Object.entries(value).forEach(([ key, value ]) => {
    if (original[key] !== value) {
      reactive[key] = value;
      return;
    }

    if (typeof value !== 'object') {
      return;
    }

    deepAssign(reactive[key], original[key], value);
  });
}

Intended outcome:

I expect the same reference can be reused (since the underlying value has not changed):

console.log(cache === store.reactiveField); // true

Actual outcome:

Reference is different every time:

console.log(cache === store.reactiveField); // false

How to reproduce the issue:

Here is the example code to reproduce:

type TestObject = { number: number };
type Parent = { object: TestObject };

const object = { number: 0 };
const parent = { object };

class Store {
  value: ParentObject = {}

  constructor() {
    makeAutoObservable(this);
  }
}

const store = new Store();
store.value = parent;
const parentCache = store.value;
const objectCache = store.value.object;
store.value = parent;

console.log(parentCache === store.value); // false
console.log(objectCache === store.value.object); // false

Versions

^6.13.1

@mweststrate
Copy link
Member

This is intentionally, to have behavior consistent throughout all APIs. If you want to share the same ref, just observable(object) (and store it!) first and then assign it to the property (e.g. const parent = observable({object})). Or, if that object doesn't have to become observable use an annotation to indicate so, for example makeAutoObservable(this, { value: observable.ref}).

https://mobx.js.org/observable-state.html#available-annotations

Please note that using observables in useEffect deps is an anti pattern, I recommend to set up a reaction inside a useEffect, and have MobX instead of React deal with the deps.

https://mobx.js.org/react-integration.html#useeffect

@andres-kovalev
Copy link
Author

Thanks for the answer, especially for the link.

We were also thinking about converting an object into an observable, but it make the parent and child components know about each other's internals: parent component should wrap an object with observable() only because child puts this object into an observable state. Probably it's better to wrap it on a child level and memoize:

const Child = ({ object }: Props) => {
  const observableObject = useMemo(() => obervable({ object }), [object]);

  ...
}

WDYT?

@mweststrate
Copy link
Member

mweststrate commented Sep 5, 2024 via email

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

No branches or pull requests

2 participants