Skip to content

Commit 3fbfb9b

Browse files
authored
Emit Activity boundaries as comments in Fizz (facebook#32834)
Uses `&` for Activity as opposed to `$` for Suspense. This will be used to delimitate which nodes we can skip hydrating. This isn't used on the client yet. It's just a noop on the client because it's just an unknown comment. This just adds the SSR parts.
1 parent 8571249 commit 3fbfb9b

File tree

7 files changed

+177
-19
lines changed

7 files changed

+177
-19
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4087,6 +4087,28 @@ export function writePlaceholder(
40874087
return writeChunkAndReturn(destination, placeholder2);
40884088
}
40894089

4090+
// Activity boundaries are encoded as comments.
4091+
const startActivityBoundary = stringToPrecomputedChunk('<!--&-->');
4092+
const endActivityBoundary = stringToPrecomputedChunk('<!--/&-->');
4093+
4094+
export function pushStartActivityBoundary(
4095+
target: Array<Chunk | PrecomputedChunk>,
4096+
renderState: RenderState,
4097+
): void {
4098+
target.push(startActivityBoundary);
4099+
}
4100+
4101+
export function pushEndActivityBoundary(
4102+
target: Array<Chunk | PrecomputedChunk>,
4103+
renderState: RenderState,
4104+
preambleState: null | PreambleState,
4105+
): void {
4106+
if (preambleState) {
4107+
pushPreambleContribution(target, preambleState);
4108+
}
4109+
target.push(endActivityBoundary);
4110+
}
4111+
40904112
// Suspense boundaries are encoded as comments.
40914113
const startCompletedSuspenseBoundary = stringToPrecomputedChunk('<!--$-->');
40924114
const startPendingSuspenseBoundary1 = stringToPrecomputedChunk(
@@ -4225,6 +4247,23 @@ export function writeEndClientRenderedSuspenseBoundary(
42254247
const boundaryPreambleContributionChunkStart = stringToPrecomputedChunk('<!--');
42264248
const boundaryPreambleContributionChunkEnd = stringToPrecomputedChunk('-->');
42274249

4250+
function pushPreambleContribution(
4251+
target: Array<Chunk | PrecomputedChunk>,
4252+
preambleState: PreambleState,
4253+
) {
4254+
// Same as writePreambleContribution but for the render phase.
4255+
const contribution = preambleState.contribution;
4256+
if (contribution !== NoContribution) {
4257+
target.push(
4258+
boundaryPreambleContributionChunkStart,
4259+
// This is a number type so we can do the fast path without coercion checking
4260+
// eslint-disable-next-line react-internal/safe-string-coercion
4261+
stringToChunk('' + contribution),
4262+
boundaryPreambleContributionChunkEnd,
4263+
);
4264+
}
4265+
}
4266+
42284267
function writePreambleContribution(
42294268
destination: Destination,
42304269
preambleState: PreambleState,

packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
createRenderState as createRenderStateImpl,
2121
pushTextInstance as pushTextInstanceImpl,
2222
pushSegmentFinale as pushSegmentFinaleImpl,
23+
pushStartActivityBoundary as pushStartActivityBoundaryImpl,
24+
pushEndActivityBoundary as pushEndActivityBoundaryImpl,
2325
writeStartCompletedSuspenseBoundary as writeStartCompletedSuspenseBoundaryImpl,
2426
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
2527
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
@@ -207,6 +209,29 @@ export function pushSegmentFinale(
207209
}
208210
}
209211

212+
export function pushStartActivityBoundary(
213+
target: Array<Chunk | PrecomputedChunk>,
214+
renderState: RenderState,
215+
): void {
216+
if (renderState.generateStaticMarkup) {
217+
// A completed boundary is done and doesn't need a representation in the HTML
218+
// if we're not going to be hydrating it.
219+
return;
220+
}
221+
pushStartActivityBoundaryImpl(target, renderState);
222+
}
223+
224+
export function pushEndActivityBoundary(
225+
target: Array<Chunk | PrecomputedChunk>,
226+
renderState: RenderState,
227+
preambleState: null | PreambleState,
228+
): void {
229+
if (renderState.generateStaticMarkup) {
230+
return;
231+
}
232+
pushEndActivityBoundaryImpl(target, renderState, preambleState);
233+
}
234+
210235
export function writeStartCompletedSuspenseBoundary(
211236
destination: Destination,
212237
renderState: RenderState,

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3669,7 +3669,7 @@ describe('ReactDOMServerPartialHydration', () => {
36693669
});
36703670

36713671
// @gate enableActivity
3672-
it('a visible Activity component acts like a fragment', async () => {
3672+
it('a visible Activity component is surrounded by comment markers', async () => {
36733673
const ref = React.createRef();
36743674

36753675
function App() {
@@ -3690,9 +3690,11 @@ describe('ReactDOMServerPartialHydration', () => {
36903690
// pure indirection.
36913691
expect(container).toMatchInlineSnapshot(`
36923692
<div>
3693+
<!--&-->
36933694
<span>
36943695
Child
36953696
</span>
3697+
<!--/&-->
36963698
</div>
36973699
`);
36983700

@@ -3739,6 +3741,8 @@ describe('ReactDOMServerPartialHydration', () => {
37393741
<span>
37403742
Visible
37413743
</span>
3744+
<!--&-->
3745+
<!--/&-->
37423746
</div>
37433747
`);
37443748

@@ -3760,6 +3764,8 @@ describe('ReactDOMServerPartialHydration', () => {
37603764
<span>
37613765
Visible
37623766
</span>
3767+
<!--&-->
3768+
<!--/&-->
37633769
<span
37643770
style="display: none;"
37653771
>

packages/react-markup/src/ReactFizzConfigMarkup.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,31 @@ export function pushSegmentFinale(
151151
return;
152152
}
153153

154+
export function pushStartActivityBoundary(
155+
target: Array<Chunk | PrecomputedChunk>,
156+
renderState: RenderState,
157+
): void {
158+
// Markup doesn't have any instructions.
159+
return;
160+
}
161+
162+
export function pushEndActivityBoundary(
163+
target: Array<Chunk | PrecomputedChunk>,
164+
renderState: RenderState,
165+
preambleState: null | PreambleState,
166+
): void {
167+
// Markup doesn't have any instructions.
168+
return;
169+
}
170+
154171
export function writeStartCompletedSuspenseBoundary(
155172
destination: Destination,
156173
renderState: RenderState,
157174
): boolean {
158175
// Markup doesn't have any instructions.
159176
return true;
160177
}
178+
161179
export function writeStartClientRenderedSuspenseBoundary(
162180
destination: Destination,
163181
renderState: RenderState,

packages/react-noop-renderer/src/ReactNoopServer.js

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ type TextInstance = {
3030
hidden: boolean,
3131
};
3232

33+
type ActivityInstance = {
34+
children: Array<Instance | TextInstance | SuspenseInstance>,
35+
};
36+
3337
type SuspenseInstance = {
3438
state: 'pending' | 'complete' | 'client-render',
3539
children: Array<Instance | TextInstance | SuspenseInstance>,
@@ -164,44 +168,74 @@ const ReactNoopServer = ReactFizzServer({
164168
});
165169
},
166170

171+
pushStartActivityBoundary(
172+
target: Array<Uint8Array>,
173+
renderState: RenderState,
174+
): void {
175+
const activityInstance: ActivityInstance = {
176+
children: [],
177+
};
178+
target.push(Buffer.from(JSON.stringify(activityInstance), 'utf8'));
179+
},
180+
181+
pushEndActivityBoundary(
182+
target: Array<Uint8Array>,
183+
renderState: RenderState,
184+
preambleState: null | PreambleState,
185+
): void {
186+
target.push(POP);
187+
},
188+
167189
writeStartCompletedSuspenseBoundary(
168190
destination: Destination,
169191
renderState: RenderState,
170-
suspenseInstance: SuspenseInstance,
171192
): boolean {
172-
suspenseInstance.state = 'complete';
193+
const suspenseInstance: SuspenseInstance = {
194+
state: 'complete',
195+
children: [],
196+
};
173197
const parent = destination.stack[destination.stack.length - 1];
174198
parent.children.push(suspenseInstance);
175199
destination.stack.push(suspenseInstance);
200+
return true;
176201
},
177202
writeStartPendingSuspenseBoundary(
178203
destination: Destination,
179204
renderState: RenderState,
180-
suspenseInstance: SuspenseInstance,
181205
): boolean {
182-
suspenseInstance.state = 'pending';
206+
const suspenseInstance: SuspenseInstance = {
207+
state: 'pending',
208+
children: [],
209+
};
183210
const parent = destination.stack[destination.stack.length - 1];
184211
parent.children.push(suspenseInstance);
185212
destination.stack.push(suspenseInstance);
213+
return true;
186214
},
187215
writeStartClientRenderedSuspenseBoundary(
188216
destination: Destination,
189217
renderState: RenderState,
190-
suspenseInstance: SuspenseInstance,
191218
): boolean {
192-
suspenseInstance.state = 'client-render';
219+
const suspenseInstance: SuspenseInstance = {
220+
state: 'client-render',
221+
children: [],
222+
};
193223
const parent = destination.stack[destination.stack.length - 1];
194224
parent.children.push(suspenseInstance);
195225
destination.stack.push(suspenseInstance);
226+
return true;
196227
},
197228
writeEndCompletedSuspenseBoundary(destination: Destination): boolean {
198229
destination.stack.pop();
230+
return true;
199231
},
200232
writeEndPendingSuspenseBoundary(destination: Destination): boolean {
201233
destination.stack.pop();
234+
return true;
202235
},
203236
writeEndClientRenderedSuspenseBoundary(destination: Destination): boolean {
204237
destination.stack.pop();
238+
return true;
205239
},
206240

207241
writeStartSegment(
@@ -218,9 +252,11 @@ const ReactNoopServer = ReactFizzServer({
218252
throw new Error('Segments are only expected at the root of the stack.');
219253
}
220254
destination.stack.push(segment);
255+
return true;
221256
},
222257
writeEndSegment(destination: Destination, formatContext: null): boolean {
223258
destination.stack.pop();
259+
return true;
224260
},
225261

226262
writeCompletedSegmentInstruction(
@@ -241,6 +277,7 @@ const ReactNoopServer = ReactFizzServer({
241277
0,
242278
...segment.children,
243279
);
280+
return true;
244281
},
245282

246283
writeCompletedBoundaryInstruction(
@@ -255,6 +292,7 @@ const ReactNoopServer = ReactFizzServer({
255292
}
256293
boundary.children = segment.children;
257294
boundary.state = 'complete';
295+
return true;
258296
},
259297

260298
writeClientRenderBoundaryInstruction(
@@ -263,6 +301,7 @@ const ReactNoopServer = ReactFizzServer({
263301
boundary: SuspenseInstance,
264302
): boolean {
265303
boundary.status = 'client-render';
304+
return true;
266305
},
267306

268307
writePreambleStart() {},

packages/react-server/src/ReactFizzServer.js

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import {
5151
import {
5252
writeCompletedRoot,
5353
writePlaceholder,
54+
pushStartActivityBoundary,
55+
pushEndActivityBoundary,
5456
writeStartCompletedSuspenseBoundary,
5557
writeStartPendingSuspenseBoundary,
5658
writeStartClientRenderedSuspenseBoundary,
@@ -2200,23 +2202,50 @@ function renderLazyComponent(
22002202
renderElement(request, task, keyPath, Component, resolvedProps, ref);
22012203
}
22022204

2203-
function renderOffscreen(
2205+
function renderActivity(
22042206
request: Request,
22052207
task: Task,
22062208
keyPath: KeyNode,
22072209
props: Object,
22082210
): void {
2209-
const mode: ?OffscreenMode = (props.mode: any);
2210-
if (mode === 'hidden') {
2211-
// A hidden Offscreen boundary is not server rendered. Prerendering happens
2212-
// on the client.
2211+
const segment = task.blockedSegment;
2212+
if (segment === null) {
2213+
// Replay
2214+
const mode: ?OffscreenMode = (props.mode: any);
2215+
if (mode === 'hidden') {
2216+
// A hidden Activity boundary is not server rendered. Prerendering happens
2217+
// on the client.
2218+
} else {
2219+
// A visible Activity boundary has its children rendered inside the boundary.
2220+
const prevKeyPath = task.keyPath;
2221+
task.keyPath = keyPath;
2222+
renderNode(request, task, props.children, -1);
2223+
task.keyPath = prevKeyPath;
2224+
}
22132225
} else {
2214-
// A visible Offscreen boundary is treated exactly like a fragment: a
2215-
// pure indirection.
2216-
const prevKeyPath = task.keyPath;
2217-
task.keyPath = keyPath;
2218-
renderNodeDestructive(request, task, props.children, -1);
2219-
task.keyPath = prevKeyPath;
2226+
// Render
2227+
// An Activity boundary is delimited so that we can hydrate it separately.
2228+
pushStartActivityBoundary(segment.chunks, request.renderState);
2229+
segment.lastPushedText = false;
2230+
const mode: ?OffscreenMode = (props.mode: any);
2231+
if (mode === 'hidden') {
2232+
// A hidden Activity boundary is not server rendered. Prerendering happens
2233+
// on the client.
2234+
} else {
2235+
// A visible Activity boundary has its children rendered inside the boundary.
2236+
const prevKeyPath = task.keyPath;
2237+
task.keyPath = keyPath;
2238+
// We use the non-destructive form because if something suspends, we still
2239+
// need to pop back up and finish the end comment.
2240+
renderNode(request, task, props.children, -1);
2241+
task.keyPath = prevKeyPath;
2242+
}
2243+
pushEndActivityBoundary(
2244+
segment.chunks,
2245+
request.renderState,
2246+
task.blockedPreamble,
2247+
);
2248+
segment.lastPushedText = false;
22202249
}
22212250
}
22222251

@@ -2291,7 +2320,7 @@ function renderElement(
22912320
return;
22922321
}
22932322
case REACT_ACTIVITY_TYPE: {
2294-
renderOffscreen(request, task, keyPath, props);
2323+
renderActivity(request, task, keyPath, props);
22952324
return;
22962325
}
22972326
case REACT_SUSPENSE_LIST_TYPE: {

packages/react-server/src/forks/ReactFizzConfig.custom.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export const pushFormStateMarkerIsNotMatching =
5959
$$$config.pushFormStateMarkerIsNotMatching;
6060
export const writeCompletedRoot = $$$config.writeCompletedRoot;
6161
export const writePlaceholder = $$$config.writePlaceholder;
62+
export const pushStartActivityBoundary = $$$config.pushStartActivityBoundary;
63+
export const pushEndActivityBoundary = $$$config.pushEndActivityBoundary;
6264
export const writeStartCompletedSuspenseBoundary =
6365
$$$config.writeStartCompletedSuspenseBoundary;
6466
export const writeStartPendingSuspenseBoundary =

0 commit comments

Comments
 (0)