main
md 253 lines 11.1 KB
Rendered Raw
1 # react-server
2
3 This is an experimental package for creating custom React streaming server renderers.
4
5 **Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.**
6
7 **Use it at your own risk.**
8
9 ## Usage
10
11 `react-server` is a package implementing various Server Rendering capabilities. The two implementation are codenamed `Fizz` and `Flight`.
12
13 `Fizz` is a renderer for Server Side Rendering React. The same code that runs in the client (browser or native) is run on the server to produce an initial view to send to the client before it has to download and run React and all the user code to produce that view on the client.
14
15 `Flight` is a renderer for React Server Components. These are components that never run on a client. The output of a React Server Component render can be a React tree that can run on the client or be SSR'd using `Fizz`.
16
17 ## `Fizz` Usage
18
19 This part of the Readme is not fully developed yet
20
21 ## `Flight` Usage
22
23 To use `react-server` for React Server Components you must set up an implementation package alongside `react-client`. Use an existing implementation such as `react-server-dom-webpack` as a guide.
24
25 You might implement a render function like
26
27 ```js
28 import {
29 createRequest,
30 startWork,
31 startFlowing,
32 stopFlowing,
33 abort,
34 } from 'react-server/src/ReactFlightServer'
35
36 function render(
37 model: ReactClientValue,
38 clientManifest: ClientManifest,
39 options?: Options,
40 ): ReadableStream {
41 const request = createRequest(
42 model,
43 clientManifest,
44 options ? options.onError : undefined,
45 options ? options.identifierPrefix : undefined,
46 options ? options.temporaryReferences : undefined,
47 __DEV__ && options ? options.environmentName : undefined,
48 __DEV__ && options ? options.filterStackFrame : undefined,
49 );
50 const stream = new ReadableStream(
51 {
52 type: 'bytes',
53 start: (controller): ?Promise<void> => {
54 startWork(request);
55 },
56 pull: (controller): ?Promise<void> => {
57 startFlowing(request, controller);
58 },
59 cancel: (reason): ?Promise<void> => {
60 stopFlowing(request);
61 abort(request, reason);
62 },
63 },
64 {highWaterMark: 0},
65 );
66 return stream;
67 }
68
69 ```
70
71 ### `Flight` Rendering
72
73 `react-server` implements the React Server Components rendering implementation. React Server Components is in essence a general purpose serialization and deserialization capability with support for some built-in React primitives such as Suspense and Lazy.
74
75 The renderable type is a superset of `structuredClone()`. In addition to all the cloneable types `react-server` can render Symbols, Promises, Iterators and Iterables, async Iterators and Iterables.
76
77 Here are some examples of what can be rendered
78 ```js
79 // primitives
80 createResponse(123, ...)
81
82 // objects and Arrays
83 createResponse({ messages: ['hello', 'react'] }, ...)
84
85 // Maps, Sets, and more
86 createResponse({ m: Map(['k', 'v'])}, ...)
87 ```
88
89 Additionally React built ins can be rendered including Function Components
90
91 Function Component are called and the return value can be any renderable type. Since `react-server` supports Promises, Function Components can be async functions.
92
93 Here are some examples of what can be rendered
94 ```js
95
96 async function App({ children }) {
97 return children
98 }
99
100 createResponse(<App ><Children /></App>, ...)
101 ```
102
103 Finally, There are two types of references in `react-server` that can be rendered
104
105 #### Client References
106 When a React Server Component framework bundles an application and encounters a `"use client"` directive it must resister exported members with `"registerClientReference"` which will encode the necessary information for `Flight` to interpret the export as a reference to be loaded on the client rather than a direct dependency on the Server module graph.
107
108 When rendering a client reference `Flight` will encode necessary information in the serialized output to describe how to load the code which represents the client module.
109
110 While it is common for client references to be components they can be any value.
111
112
113 ```js
114 'use client'
115
116 export function alert(message) {
117 alert(message)
118 }
119 ```
120
121 ```js
122 'use client'
123
124 export function ClientComp({ onClick, message }) {
125 return <button onClick={onClick}>Alert</button>
126 }
127 ```
128
129 ```js
130
131 // client references don't have to just be components, anything can be
132 // a reference, in this case we're importing a function that will be
133 // passed to the ClientComp component
134 import { alert } from '...'
135 import { ClientComp } from '...'
136
137 async function App({ children }) {
138 return children
139 }
140
141 createResponse(
142 <App >
143 <ClientComp onClick={alert} message={"hello world"} />
144 </App>,
145 ...)
146 ```
147
148 #### Server References
149 Similarly When a React Server Component framework bundles an application and encounters a `"use server"` directive in a file or in a function body, including closures, it must implement that function as as a server entrypoint that can be called from the client. To make `Flight` aware that a function is a Server Reference the function should be registered with `registerServerReference()`.
150
151 ```js
152
153 async function logOnServer(message) {
154 "use server"
155 console.log(message)
156 }
157
158 async function App({ children }) {
159 // logOnServer can be used in a Server Component
160 logOnServer('used from server')
161 return children
162 }
163
164 createResponse(
165 <App >
166 <ClientComp onClick={logOnServer} message={"used from client"} />
167 </App>,
168 ...)
169 ```
170
171 ### `Flight` Prerendering
172
173 When rendering with `react-server` there are two broad contexts when this might happen. Realtime when responding to a user request and ahead of time when prerendering a page that can later be used more than once.
174
175 While the core rendering implementation is the same in both cases there are subtle differences we can adopt that take advantage of the context. For instance while rendering in response to a real user request we want to stream eagerly if the consumer is requesting information. This allows us to stream content to the consumer as it becomes available but might have implications for the stability of the serialized format. When prerendering we assume there is not urgency to producing a partial result as quickly as possible so we can alter the internal implementation take advantage of this. To implement a prerender API use `createPrerenderRequest` in place of `createRequest`.
176
177 One key semantic change prerendering has with rendering is how errors are handled. When rendering an error is embedded into the output and must be handled by the consumer such as an SSR render or on the client. However with prerendering there is an expectation that if the prerender errors then the entire prerender will be discarded or it will be used but the consumer will attempt to recover that error by asking for a dynamic render. This is analogous to how errors during SSR aren't immediately handled they are actually encoded as requests for client recovery. The error only is observed if the retry on the client actually fails. To account for this prerenders simply omit parts of the model that errored. you can use the `onError` argument in `createPrerenderRequest` to observe if an error occurred and users of your `prerender` implementation can choose whether to abandon the prerender or implement dynamic recovery when an error occurs.
178
179 Existing implementations only return the stream containing the output of the prerender once it has completed. In the future we may introduce a `resume` API similar to the one that exists for `Fizz`. In anticipation of such an API it is expected that implementations of `prerender` return the type `Promise<{ prelude: <Host Appropriate Stream Type> }>`
180
181 ```js
182 function prerender(
183 model: ReactClientValue,
184 clientManifest: ClientManifest,
185 options?: Options,
186 ): Promise<StaticResult> {
187 return new Promise((resolve, reject) => {
188 const onFatalError = reject;
189 function onAllReady() {
190 const stream = new ReadableStream(
191 {
192 type: 'bytes',
193 start: (controller): ?Promise<void> => {
194 startWork(request);
195 },
196 pull: (controller): ?Promise<void> => {
197 startFlowing(request, controller);
198 },
199 cancel: (reason): ?Promise<void> => {
200 stopFlowing(request);
201 abort(request, reason);
202 },
203 },
204 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
205 {highWaterMark: 0},
206 );
207 resolve({prelude: stream});
208 }
209 const request = createPrerenderRequest(
210 model,
211 clientManifest,
212 onAllReady,
213 onFatalError,
214 options ? options.onError : undefined,
215 options ? options.identifierPrefix : undefined,
216 options ? options.temporaryReferences : undefined,
217 __DEV__ && options ? options.environmentName : undefined,
218 __DEV__ && options ? options.filterStackFrame : undefined,
219 );
220 startWork(request);
221 });
222 }
223 ```
224
225 ## `Flight` Reference (Incomplete)
226
227 ### `createRequest(model, bundlerConfig, ...options): Request`
228
229 The signature of this method changes as we evolve the project so this Readme will omit the specific signature but generally this function will produce a Request that represents the rendering of some React application (the model) along with implementation specific bundler configuration. Typically this configuration will tell the `Flight` implementation how to encode Client References in the serialized output
230
231 The `RequestInstance` represents the render.
232
233 Rendering does not actually begin until you call `startWork`
234
235 ### `createPrerenderRequest(model, bundlerConfig, ...options): Request`
236
237 This is similar to `createRequest` but it alters some internal semantics for how errors and aborts are treated. It returns the same type as `createRequest`.
238
239 ### `startWork(request: Request): void`
240
241 When passed a request this will initiate the actual render. It will continue until it completes
242
243 ### `startFlowing(request: Request, destination: Destination): void`
244
245 a destination is whatever the implementation wants to use for storing the output of the render. In existing implementations it is either a Node stream or a Web stream. When you call `startFlowing` the request will write to the destination continuously whenever more chunks are unblocked, say after an async function has resolved and there is something new to serialize. You can implement streaming backpressure using `stopFlowing()`
246
247 ### `stopFlowing(request: Request): void`
248
249 If you need to pause or permanently end the writing of any additional serialized output for this request you can call `stopFlowing(request)`. You may start flowing again after you've stopped. This is how you would implement backpressure support for streams for instance. It's important to note that stopping flowing is not going to stop rendering. If you want rendering to stop you must `abort` the request.
250
251 ### `abort(request: Request): void`
252
253 If you want to stop rendering you can abort the request with `abort(request)`. This will cause all incomplete work to be abandoned. If the request was created with `createRequest` the abort will encode errors into any unfinished slots in the serialization. If the request was created with `createPrerenderRequest` the abort will omit anything in the places that are unfinished leaving the serialized model in an incomplete state.