realm-js icon indicating copy to clipboard operation
realm-js copied to clipboard

Spreading a Realm.Object gives in an empty object

Open kraenhansen opened this issue 4 years ago • 11 comments

Goals

I would like to be able to easily convert a Realm.Object to a plain-ol'-javascript-object.

Expected Results

When doing a JS spread on a Realm.Object I would expect the resulting object contained the same properties as the Realm.Object.

Actual Results

You get an empty object.

Steps to Reproduce & Code Sample

const Realm = require('realm')
const realm = new Realm({ schema: [ { name: "Person", properties: { name: "string" } } ] });
realm.write(() => { realm.create("Person", { name: "Alice" }) });
const persons = realm.objects("Person"); // 👉Results { [0]: RealmObject { [name]: 'Alice' } }
const alice = persons[0]; // 👉RealmObject { [name]: 'Alice' }
Object.keys(alice) // 👉[ 'name' ]
Object.getOwnPropertyNames(alice) // 👉[ '_realm', 'name' ]
alice.hasOwnProperty('name') // 👉true
Object.getOwnPropertyDescriptors(alice)
/* 👇
{ _realm:
   { value: Realm {},
     writable: true,
     enumerable: false,
     configurable: false },
  name:
   { value: 'Alice',
     writable: true,
     enumerable: false,
     configurable: true } }
*/
{ ...alice } // 👉{}

An interesting observation is that getOwnPropertyNames contains 'name' but Object.hasOwnProperty(alice, 'name') is false 🤷‍♂️

Version of Realm and Tooling

  • Realm JS SDK Version: 3.5.0
  • Node or React Native: Node v10.17.0
  • Client OS & Version: N/A
  • Which debugger for React Native: None

kraenhansen avatar Dec 06 '19 14:12 kraenhansen

Spread operator is very tricky and makes confusing about behaviour. It doesn't copy getters/setters, just values: https://2ality.com/2016/10/rest-spread-properties.html#both-spread-and-object.assign()-read-values-via-a-“get”-operation and implentation in RealmObject class https://github.com/realm/realm-js/blob/cd579770ad50bba5ce6dcfa89591334fd1735c53/lib/browser/objects.js#L59

Anyway. hasOwnProperty is not class method, it is object method. You need to call this like this alice.hasOwnProperty('name') or Object.prototype.hasOwnProperty.call(alice, 'name')

radeno avatar Dec 10 '19 09:12 radeno

@radeno thanks for your interest.

To me the spread operator is pretty straight forward - it makes sense to me that it doesn't copy getters/setters, but this is also not what this issue is about. Thanks for the hint on the use of hasOwnProperty - but honestly, that was just there to show what the value might be: I've updated the code.

I believe the root cause of this issue is that both spread and Object.assign() only consider own enumerable properties:

Object.getOwnPropertyDescriptors(alice)
/* 👇
{ _realm:
   { value: Realm {},
     writable: true,
     enumerable: false,
     configurable: false },
  name:
   { value: 'Alice',
     writable: true,
     enumerable: false,
     configurable: true } }
*/

kraenhansen avatar Dec 10 '19 11:12 kraenhansen

Same issue here, tested on RN "realm": "ˆ5.0.3" "react": "^16.13.1" "react-native": "0.62.2"

I'trying to load an Object to a const, that I'm using with the useState hook, that's because i can change the full object fields with just one useState setter callback const testobj = realm.objectForPrimaryKey(... Then if i spread testobj the return value is empty {} Also I've try to take the first element from objects, same result, spread is empty const testobj = realm.objects(... )[0]

I've try the JSON.parse(JSON.stringify(testobj)) trick, but I've no luck with dates that became strings, and also it's not that beauty. Any other suggestion to stread a Realm.Object or a Realm.Result?

thank you!

davidegironi avatar Apr 24 '20 15:04 davidegironi

For those who are interested in, I've found a quick fix here, I'm testing it (mapProps method) https://github.com/realm/realm-js/issues/141#issuecomment-340663517

davidegironi avatar Apr 24 '20 16:04 davidegironi

Even i'm facing the same issue where hasOwnProperty throw false.

Akash-T2S avatar Oct 22 '20 09:10 Akash-T2S

Created an alternative solution in typescript. Haven't tried using shallow clone, but deep clone seems to do the job. Obviously you can implement your own cloneDeep function, but lodash is used here out of pure laziness.

import cloneDeep from 'lodash-es/cloneDeep';

export function deepCloneRealmObject<T>(realmObject: Realm.Object): T {
    const clone: any = {};
    realmObject.keys().forEach((key) => {
        if (typeof realmObject[key] == 'object') {
            clone[key] = cloneDeep(realmObject[key]);
        } else {
            clone[key] = realmObject[key];
        }
    });

    return clone;
}

Bandwagoner avatar Oct 23 '20 00:10 Bandwagoner

This works for me.

let data = realm.objectForPrimaryKey('User', 10);

data = JSON.parse(JSON.stringify(data));

console.log({...data, name: 'Bob'});

hossein-zare avatar Jan 04 '21 14:01 hossein-zare

We found lodash functions such as orderBy and isEmpty not working with realmObject anymore which worked well with the legacy realm version.

BruceSuperProgramer avatar May 28 '21 06:05 BruceSuperProgramer

import Realm from 'realm'

export function shallowCloneRealmObject<T extends Realm.Object>(x: T) {
  const copy: any = {}
  for (const key in x) {
    copy[key] = x[key]
  }
  return copy as Exclude<T, Realm.Object>
}

My workaround for now:

  • It's a shallow clone so very close to spread
  • The cloned object won't be a live realm object which is what you would expect when spreading. This is also reflected by the types.

mfbx9da4 avatar Feb 09 '22 14:02 mfbx9da4

If people are interested I published to npm an eslint-plugin that bans spread on Realm.Object type. https://www.npmjs.com/package/@waltari/eslint-plugin-ban-realm-spread

(hopefully spread operator wont be fixed now the next day)

Waltari10 avatar Apr 29 '22 08:04 Waltari10

Note that JSON.parse(JSON.stringify(data)) will convert all ObjectIds to strings.

greenafrican avatar Aug 02 '22 14:08 greenafrican

Version 12.0.0 introduced support for the spread operator

kneth avatar Sep 11 '23 13:09 kneth