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

Incorrect wrapping of proxy objects on every render #4513

Closed
mfbx9da4 opened this issue Apr 21, 2022 · 12 comments · Fixed by #4958
Closed

Incorrect wrapping of proxy objects on every render #4513

mfbx9da4 opened this issue Apr 21, 2022 · 12 comments · Fixed by #4958
Assignees

Comments

@mfbx9da4
Copy link

mfbx9da4 commented Apr 21, 2022

How frequently does the bug occur?

All the time

Description

https://github.com/realm/realm-js/blob/master/packages/realm-react/src/useObject.tsx#L78 @takameyer

// Wrap object in a proxy to update the reference on rerender ( should only rerender when something has changed )
return new Proxy(object, {});

The intention of this code is that the reference will only change if the object has changed to play nicely with React's immutability conventions. However, the hook is not invoked only when the hook fires. It is invoked anytime the parent component renders. The component could be rendering because of a totally unrelated change to some other component state which will cause unnecessary Proxy wrapping, unnecessarily new references, break React's immutability conventions and contradicts the intention of this code.

Easier to explain with code

const initialObject = {};
function useObject() {
  return new Proxy(initialObject, {});
}

export default function App() {
  const [count, setCount] = useState(0);
  const object = useObject();
  useEffect(() => {
    // This effect will be invoked everytime the component
    // renders (eg click the button), even though the object
    // never actually changed
    console.log("object changed", object);
  }, [object]);

  return (
    <div className="App">
      <h1>{count}</h1>
      <button onClick={() => setCount((x) => x + 1)}>Click me</button>
    </div>
  );
}

https://codesandbox.io/s/modern-field-7ety98?file=/src/App.js:69-642

The fix here is to create the new proxy in the updateCallback.

I think a further improvement would be to have a singleton observable object with a single realm listener with a reference count referring to the number of active hooks using it. This would:

Side issue: while the object cache and collection will make great strides to improving immutability, non-primitive values such as Dates, sets and dictionaries will not be immutably cached correctly. Are you aware of that?

Stacktrace & log output

No response

Can you reproduce the bug?

Yes, always

Reproduction Steps

No response

Version

main

What SDK flavour are you using?

Local Database only

Are you using encryption?

No, not using encryption

Platform OS and version(s)

ios

Build environment

Which debugger for React Native: ..

Cocoapods version

No response

@takameyer
Copy link
Contributor

@mfbx9da4 Good catch. Really appreciate the feedback. We will investigate this as time allows and get back to you when we have something in review.

@takameyer takameyer self-assigned this Apr 22, 2022
@takameyer
Copy link
Contributor

@mfbx9da4 We have a good idea for fixing the proxy wrapper. That being said, can you please make a separate issue for:

Side issue: while the object cache and collection will make great strides to improving immutability, non-primitive values such as Dates, sets and dictionaries will not be immutably cached correctly. Are you aware of that?

We would also appreciate an example where this is not being handled correctly 🙂 Thanks so much for your feedback!

mfbx9da4 added a commit to mfbx9da4/realm-js that referenced this issue Apr 22, 2022
@mfbx9da4 mfbx9da4 mentioned this issue Apr 22, 2022
@mfbx9da4
Copy link
Author

@takameyer what do you make of my singleton object proposal? Is there somewhere better to discuss that?

@takameyer
Copy link
Contributor

@mfbx9da4 I think it's worth looking into. We have discussed various ways we could improve or notification systems. I think making a feature request would be a good way to invoke more discussion.

@maxencehenneron
Copy link

Is there any progress on this? In my app, I do a CPU-intensive operation using the data stored in the database. I tried the following:

const result = useMemo(() => {
   console.log('starting CPU intensive operation');
   return intensiveOperation(object);
}, [object]);

but it seems like this if fired every time the component state changes, not when the realm "object" dependency changes.

Is it what's described in this ticket?

@tomduncalf
Copy link
Contributor

It looks like the same thing that is described @maxencehenneron, yes. There's a PR #4519 with a fix for this issue which we are reviewing right now.

@sdandois
Copy link
Contributor

sdandois commented Jun 22, 2022

Hi, I noticed that useQuery has the same problem.

Is there a previous version where this works correctly? The README file caught my attention:

useQuery

Returns Realm.Results from a given type. This Hook will update on any changes to any Object in the Collection and return an empty array if the Collection is empty. The result of this can be consumed directly by the data argument of any React Native VirtualizedList or FlatList. If the component used for the list's renderItem prop is wrapped with React.Memo, then only the modified object will re-render.

const Component = () => {
  // ObjectClass is a class extending Realm.Object, which should have been provided in the Realm Config.
  // It is also possible to use the model's name as a string ( ex. "Object" ) if you are not using class based models.
  const collection = useQuery(ObjectClass);

  // The methods `sorted` and `filtered` should be wrapped in a useMemo.
  const sortedCollection = useMemo(() => collection.sorted(), [collection]);

  return (
    <FlatList data={sortedCollection} renderItem={({ item }) => <Object item={item}/>
  )
}

That is not working as expected, all items in my list are being re-rendered.

@sdandois
Copy link
Contributor

As a workaround, I've implemented a caching hook on top of the ones provided by @realm/react:

export const createRefCache = object => {
  return new Proxy(
    {},
    {
      get: function (target, property) {
        return object[property];
      },
      set: function (target, property, value) {
        object[property] = value;
      },
    },
  );
};

export const useCachedObject = (object, otherDeps = []) => {
  const [counter, setCounter] = useState(0);
  const forceRender = () => setCounter(n => n + 1);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  const memoObject = useMemo(() => createRefCache(object), [counter, ...otherDeps]);

  useEffect(() => {
    const listener = (obj, changes) => {
      if (changes.deletions.length || changes.insertions.length || changes.modifications.length) {
        forceRender();
      }
    };
    memoObject.addListener(listener);

    return () => {
      memoObject.removeListener(listener);
    };
  }, [memoObject]);

  return memoObject;
};

Let me hear what you think about it!

takameyer pushed a commit that referenced this issue Sep 29, 2022
* Update useObject and useQuery to wrap their return value with proxy only when it needs to be rerendered
@takameyer takameyer mentioned this issue Sep 29, 2022
12 tasks
takameyer pushed a commit that referenced this issue Sep 30, 2022
* Update useObject and useQuery to wrap their return value with proxy only when it needs to be rerendered
takameyer pushed a commit that referenced this issue Sep 30, 2022
* Update useObject and useQuery to wrap their return value with proxy only when it needs to be rerendered
takameyer added a commit that referenced this issue Oct 5, 2022
* Fix #4513

* Update useObject and useQuery to wrap their return value with proxy only when it needs to be rerendered

* Update CHANGELOG and cleanup code

Co-authored-by: David Alberto Adler <dalberto.adler@gmail.com>
@takameyer
Copy link
Contributor

This has been fixed in @realm/react@0.4.0

@mordechaim
Copy link

@takameyer
I'm not sure what I'm missing, but I clearly still observe the issue described in #4913 for both useQuery and useObject.

Android 10

"react-native": "^0.70.5",
"realm": "^11.2.0",
"@realm/react": "^0.4.1"

@dimonnwc3
Copy link

I still experience issue for both useQuery and useObject

@channeladam
Copy link

Argh, same. Just stumbled upon this issue after creating #5264 .

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants