hrtyy.dev

RSC Payload & Serialized Props

React Server Components Payload and Serialized Props

Introduction

There are terms of React Server and React Client. According to this discussion, React Server means only the RSC server environment and React Client means any environment that consumes the React Server output.

SSR server uses the output to generate HTML or web browser uses it to update the DOM.

In Next.js document, the output is called RSC Payload. (I couldn't find the term in React official document.)

RSC Payload contains any props passed from a Server Component to a Client Component. Then, the props are serialized.

I noticed that this process is clever, so I will explain it briefly.

Customize JSON.stringify()

First, JavaScript's JSON.stringify() won't serialize some objects in default.

1let s = new Set([1, 2, 3])
2let p = new Promise(resolve => resolve([1, 2, 3]))
3let a = [1, 2, 3]
4let j = JSON.stringify({s, p, a})
5
6console.log(j)
7// '{"s":{},"p":{},"a":[1,2,3]}'

React uses same method to serialize props, but it can serialize Set and Promise. We can change the behavior of JSON.stringify() passing a mapper function to the second argument. React uses it. In this custom mapper, the passed value is checked whether it is supported type or not. Set, Promise (Thenable), Map or some other types are seemed to be supported.

Check the content of if block of Set. It returns a custom id calling serializeSetID. This is a simple function that returns a passed id with prefix of "$W".

  • Set: $W
  • Promise: $@

serializePromiseID adds "$@" prefix to the passed id.

React can deserialize json string using the prefix and restore the original object.

Investigate the RSC Payload

We use Next.js and define two simple components to check actual payload.

app/page2/_components/Client2.tsx
1"use client";
2
3import { PropsWithChildren } from "react";
4
5export function Client2({
6 testSet,
7 testPromise,
8}: PropsWithChildren<{
9 testSet: Set<string>;
10 testPromise: Promise<string>;
11}>) {
12 return (
13 <div>
14 <div>
15 {testSet.constructor.name} {testSet}
16 </div>
17 <div>
18 {testPromise.constructor.name} {testPromise}
19 </div>
20 </div>
21 );
22}
app/page2/page.tsx
1import { Client2 } from "@/app/_components/Client2";
2
3function createPromise(): Promise<string> {
4 return new Promise((resolve) => {
5 setTimeout(() => {
6 resolve("data from page");
7 }, 1000);
8 });
9}
10
11export default function Page() {
12 const set = new Set(["set1", "set2", "set1"]);
13 return (
14 <Client2
15 testSet={set}
16 testPromise={createPromise()}
17 />
18 );
19}

Add a Link component to some page which has href to /page2. Next.js will prefetch RSC payload of this page.

11:....
20:....
34:....
45:....
56:....
68:....
79:["set1","set2"]
82:[null, ... ","childProp":{"current":["$L7",["$","$L8",null,{"testSet":"$W9","testPromise":"$@a"}],null],"segment":"__PAGE__"},"styles":[]}],"segment":"page2"},"styles":[]}],"params":{}}],null]
93:....
107:null
11a:"data from page"

I removed some unnecessary parts of the payload. The important parts are testSet and testPromise inside childProp JSON. Each of them has values "$W9" and "$@a" respectively.

As described above, "$W" and "$@" are prefixes of Set and Promise. The remaining part indicates the id of the actual data. This payload includes set data with id "9" and promise data with id "a".

If you have any questions, please send an email tohoritayuya@gmail.com.