puck icon indicating copy to clipboard operation
puck copied to clipboard

Class instances loose non enumerable properties when used as values

Open Bilboramix opened this issue 3 months ago • 3 comments

Description

Since Puck 0.19.1, date are broken in props, even in nested objects. I also checked current version (0.20.2), it's still there.

Environment

  • Puck versions: 0.18.3 (working), 0.19.1 (breaking point)

Steps to reproduce

  • Install reproduction repo : https://github.com/Bilboramix/test-puck
  • Run it (it have puck 0.18.3)
  • Put a TestBlock in the editor and click buttons in the fields
  • The dates should appears in both cases
  • Upgrade to puck 19.1 and reproduce from step 2
  • An empty object should appears in both cases

Output

With 18.3 :

Image

With 19.1 and 20.2:

Image

Bilboramix avatar Sep 20 '25 12:09 Bilboramix

Hey @Bilboramix!

TL;DR

To work around this, use serializable values for your Puck data (plain JSON only). You will need to do this anyway when persisting the data.

onChange(myDate.toISOString()) // or myDate.getTime()

Details for future reference

Thanks for bringing this up. This is happening because Puck could previously handle non-enumerable object members as values before 0.19.x. I suspect this changed with the performance optimizations.

This affects not only Date objects, but any class instance with non-enumerable members. That includes private fields and methods, as well as values defined on the prototype. Once you call onChange with a class instance, it stops behaving as an instance of that class all together.

With Date, I suspect the value is being spread. Spreading only copies enumerable properties, which do not include a date’s private fields or methods. As a result, when you read the value back in the render function, you get an empty object, because dates are highly encapsulated and have no public enumerable data.

Here is a minimal example that shows this with a custom class, in this case, the instance that is not sent to the onChange gets stringified as expected because the toJSON method is present, but when passed to the onChange and read from the value, only the enumerable property is shown:

class MyClass {
  enumerable: string;
  #nonEnumerable: string;

  constructor() {
    this.enumerable = "Hello from public";
    this.#nonEnumerable = "Hello from private";
  }

  // Custom JSON.stringify behavior, non-enumerable method
  toJSON(): string {
    return this.#nonEnumerable;
  }
}

const config: Config = {
  components: {
    TestBlock: {
      fields: {
        test: {
          type: "custom",
          render: ({ value, onChange }) => {
            const onClickTest = () => onChange(new MyClass());

            const classInstance = new MyClass();

            return (
              <div style={{ color: "black" }}>
                <button onClick={onClickTest}>Click !</button>
                <hr />
                {value && (
                  <div>
                    Stringified Value : {JSON.stringify(value)} <br />
                    Is value instance of MyClass: {String(value instanceof MyClass)}
                  </div>
                )}
                <hr />
                <div>
                    Stringified Class : {JSON.stringify(classInstance)} <br />
                    Is Class instance of MyClass: {String(classInstance instanceof MyClass)}
                </div>
              </div>
            );
          },
        },
      },
      render: ({ children }) => {
        return <h1>{children}</h1>;
      },
    },
  },
};
Image

FedericoBonel avatar Oct 01 '25 10:10 FedericoBonel

Good to know. Thanks a lot for your investigations 💪 It's okay for me to have this behavior, but for future reader can you tell if there will be any change about this ?

Bilboramix avatar Oct 02 '25 00:10 Bilboramix

Yes, this is a valid issue. Puck used to handle this, so it is worth investigating and fixing. We will keep this issue open and marked as "ready" so that anyone who wants to take a look and propose a fix is free to do so 😁.

FedericoBonel avatar Oct 02 '25 02:10 FedericoBonel