-
-
Notifications
You must be signed in to change notification settings - Fork 634
/
Copy pathRSCClientRoot.ts
123 lines (109 loc) · 4.03 KB
/
RSCClientRoot.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
'use client';
/* eslint-disable no-underscore-dangle */
import * as React from 'react';
import * as ReactDOMClient from 'react-dom/client';
import { createFromReadableStream } from 'react-on-rails-rsc/client';
import { fetch } from './utils';
import transformRSCStreamAndReplayConsoleLogs from './transformRSCStreamAndReplayConsoleLogs';
import { RailsContext, RenderFunction, RSCPayloadChunk } from './types';
const { use } = React;
if (typeof use !== 'function') {
throw new Error('React.use is not defined. Please ensure you are using React 19 to use server components.');
}
declare global {
interface Window {
__FLIGHT_DATA: unknown[];
}
}
export type RSCClientRootProps = {
componentName: string;
rscPayloadGenerationUrlPath: string;
componentProps?: unknown;
};
const createFromFetch = async (fetchPromise: Promise<Response>) => {
const response = await fetchPromise;
const stream = response.body;
if (!stream) {
throw new Error('No stream found in response');
}
const transformedStream = transformRSCStreamAndReplayConsoleLogs(stream);
return createFromReadableStream<React.ReactNode>(transformedStream);
};
const fetchRSC = ({ componentName, rscPayloadGenerationUrlPath, componentProps }: RSCClientRootProps) => {
const propsString = JSON.stringify(componentProps);
const strippedUrlPath = rscPayloadGenerationUrlPath.replace(/^\/|\/$/g, '');
return createFromFetch(fetch(`/${strippedUrlPath}/${componentName}?props=${propsString}`));
};
const createRSCStreamFromPage = () => {
let streamController: ReadableStreamController<RSCPayloadChunk> | undefined;
const stream = new ReadableStream<RSCPayloadChunk>({
start(controller) {
if (typeof window === 'undefined') {
return;
}
const handleChunk = (chunk: unknown) => {
controller.enqueue(chunk as RSCPayloadChunk);
};
if (!window.__FLIGHT_DATA) {
window.__FLIGHT_DATA = [];
}
window.__FLIGHT_DATA.forEach(handleChunk);
window.__FLIGHT_DATA.push = (...chunks: unknown[]) => {
chunks.forEach(handleChunk);
return chunks.length;
};
streamController = controller;
},
});
if (typeof document !== 'undefined' && document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
streamController?.close();
});
} else {
streamController?.close();
}
return stream;
};
const createFromRSCStream = () => {
const stream = createRSCStreamFromPage();
const transformedStream = transformRSCStreamAndReplayConsoleLogs(stream);
return createFromReadableStream<React.ReactNode>(transformedStream);
};
/**
* RSCClientRoot is a React component that handles client-side rendering of React Server Components (RSC).
* It manages the fetching, caching, and rendering of RSC payloads from the server.
*
* This component:
* 1. Fetches RSC payloads from the server using the provided URL path
* 2. Caches the responses to prevent duplicate requests
* 3. Transforms the response stream to replay server-side console logs
* 4. Uses React.use() to handle the async data fetching
*
* @requires React 19+
* @requires react-on-rails-rsc
*/
const RSCClientRoot: RenderFunction = async (
{ componentName, rscPayloadGenerationUrlPath, componentProps }: RSCClientRootProps,
_railsContext?: RailsContext,
domNodeId?: string,
) => {
if (!domNodeId) {
throw new Error('RSCClientRoot: No domNodeId provided');
}
const domNode = document.getElementById(domNodeId);
if (!domNode) {
throw new Error(`RSCClientRoot: No DOM node found for id: ${domNodeId}`);
}
if (domNode.innerHTML) {
const root = await createFromRSCStream();
ReactDOMClient.hydrateRoot(domNode, root);
} else {
const root = await fetchRSC({ componentName, rscPayloadGenerationUrlPath, componentProps });
ReactDOMClient.createRoot(domNode).render(root);
}
// Added only to satisfy the return type of RenderFunction
// However, the returned value of renderFunction is not used in ReactOnRails
// TODO: fix this behavior
return '';
};
export default RSCClientRoot;