remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
28.28k stars 2.37k forks source link

Single Fetch: json serialization is unstable #9421

Open lilouartz opened 1 month ago

lilouartz commented 1 month ago

Reproduction

https://stackblitz.com/edit/remix-run-remix-ioniz1?file=app%2Froutes%2F_index.tsx

System Info

System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 18.18.0 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.3 - /usr/local/bin/npm
    pnpm: 8.15.6 - /usr/local/bin/pnpm
  npmPackages:
    @remix-run/dev: * => 2.9.2 
    @remix-run/node: * => 2.9.2 
    @remix-run/react: * => 2.9.2 
    @remix-run/serve: * => 2.9.2 
    vite: ^5.1.0 => 5.2.11

Used Package Manager

npm

Expected Behavior

No error

Actual Behavior

Error:

Warning: Prop `dangerouslySetInnerHTML` did not match. Server: "[{\"@context\":\"https://schema.org\",\"name\":\"Hello, World!\"}]" Client: "[{\"name\":\"Hello, World!\",\"@context\":\"https://schema.org\"}]"

Note that the reason I am passing meta as loader property is because it was described as solution to https://github.com/remix-run/remix/issues/9418

The issue appears because serialized JSON values have different property order.

lilouartz commented 1 month ago

I can somewhat workaround this by changing my server-side object declaration to match what client renders, i.e.

{
  name: 'Hello, World!',
  '@context': 'https://schema.org',
},

But this is finicky and problematic because I have ESLint rule that auto sorts properties alphabetically.

brophdawg11 commented 2 weeks ago

This looks like it's coming from internal JSON.parse behavior that must order numeric object keys:

Screenshot 2024-05-28 at 11 33 29 AM

@jacob-ebey What do you think about prefixing those with _ or something in turbo-stream to make them non-numeric?

lilouartz commented 2 weeks ago

I think in my case it is the properties that start with @.

brophdawg11 commented 2 weeks ago

It's not related to the name of the key. It's due to the reuse of a name property that was previously used. turbo-stream dedupes keys and values to keep the payload minimal, and it tracks them basically with an incrementing integer. When you return something like the following, the first occurrence of name gets assigned a lower integer than @context and then the second occurrence of name re-uses the earlier/lower id.

export const loader = () => {
  return {
    meta: [
      { 
        name: 'description', 
        // ^ `name` here gets assigned a lower numeric id since it's earlier
        content: 'Welcome to Remix!',
     },
      {
        'script:ld+json': [
          {
            '@context': 'https://schema.org',
             // ^ `@context` here gets assigned a higher numeric id since it's later
             name: 'Hello, World!',
             // ^ `name` here reuses the earlier (lower) numeric id
          },
        ],
      },
    ],
  };
};

This results in the problem I described above where we stringify an object with a higher numeric key, followed by a lower numeric key - and JSON.parse re-orders them when parsing, moving name before @context