Skip to content

Commit

Permalink
[Fizz] Add FormatContext and Refactor Work (#21103)
Browse files Browse the repository at this point in the history
* Add format context

* Let the Work node hold all working state for the recursive loop

Stacks are nice and all but there's a cost to maintaining each frame
both in terms of stack size usage and writing to it.

* Move current format context into work

* Synchronously render children of a Suspense boundary

We don't have to spawn work and snapshot the context. Instead we can try
to render the boundary immediately in case it works.

* Lazily create the fallback work

Instead of eagerly create the fallback work and then immediately abort it.
We can just avoid creating it if we finish synchronously.
  • Loading branch information
sebmarkbage authored Mar 26, 2021
1 parent 1b7e471 commit 38a1aed
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 58 deletions.
6 changes: 5 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
abort,
} from 'react-server/src/ReactFizzServer';

import {createResponseState} from './ReactDOMServerFormatConfig';
import {
createResponseState,
createRootFormatContext,
} from './ReactDOMServerFormatConfig';

type Options = {
identifierPrefix?: string,
Expand Down Expand Up @@ -46,6 +49,7 @@ function renderToReadableStream(
children,
controller,
createResponseState(options ? options.identifierPrefix : undefined),
createRootFormatContext(), // We call this here in case we need options to initialize it.
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
Expand Down
6 changes: 5 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
abort,
} from 'react-server/src/ReactFizzServer';

import {createResponseState} from './ReactDOMServerFormatConfig';
import {
createResponseState,
createRootFormatContext,
} from './ReactDOMServerFormatConfig';

function createDrainHandler(destination, request) {
return () => startFlowing(request);
Expand Down Expand Up @@ -46,6 +49,7 @@ function pipeToNodeWritable(
children,
destination,
createResponseState(options ? options.identifierPrefix : undefined),
createRootFormatContext(), // We call this here in case we need options to initialize it.
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
Expand Down
52 changes: 51 additions & 1 deletion packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import escapeTextForBrowser from './escapeTextForBrowser';
import invariant from 'shared/invariant';

// Per response,
// Per response, global state that is not contextual to the rendering subtree.
export type ResponseState = {
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
Expand Down Expand Up @@ -50,6 +50,56 @@ export function createResponseState(
};
}

// Constants for the namespace we use. We don't actually provide the namespace but conditionally
// use different segment parents based on namespace. Therefore we use constants instead of the string.
const ROOT_NAMESPACE = 0; // At the root we don't need to know which namespace it is. We just need to know that it's already the right one.
const HTML_NAMESPACE = 1;
const SVG_NAMESPACE = 2;
const MATHML_NAMESPACE = 3;

type NamespaceFlag = 0 | 1 | 2 | 3;

// Lets us keep track of contextual state and pick it back up after suspending.
export type FormatContext = {
namespace: NamespaceFlag, // root/svg/html/mathml
selectedValue: null | string, // the selected value(s) inside a <select>, or null outside <select>
};

function createFormatContext(
namespace: NamespaceFlag,
selectedValue: null | string,
): FormatContext {
return {
namespace,
selectedValue,
};
}

export function createRootFormatContext(): FormatContext {
return createFormatContext(ROOT_NAMESPACE, null);
}

export function getChildFormatContext(
parentContext: FormatContext,
type: string,
props: Object,
): FormatContext {
switch (type) {
case 'select':
return createFormatContext(
parentContext.namespace,
props.value != null ? props.value : props.defaultValue,
);
case 'svg':
return createFormatContext(SVG_NAMESPACE, null);
case 'math':
return createFormatContext(MATHML_NAMESPACE, null);
case 'foreignObject':
return createFormatContext(HTML_NAMESPACE, null);
}
return parentContext;
}

// This object is used to lazily reuse the ID of the first generated node, or assign one.
// We can't assign an ID up front because the node we're attaching it to might already
// have one. So we need to lazily use that if it's available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,33 @@ export function createResponseState(): ResponseState {
};
}

// isInAParentText
export type FormatContext = boolean;

export function createRootFormatContext(): FormatContext {
return false;
}

export function getChildFormatContext(
parentContext: FormatContext,
type: string,
props: Object,
): FormatContext {
const prevIsInAParentText = parentContext;
const isInAParentText =
type === 'AndroidTextInput' || // Android
type === 'RCTMultilineTextInputView' || // iOS
type === 'RCTSinglelineTextInputView' || // iOS
type === 'RCTText' ||
type === 'RCTVirtualText';

if (prevIsInAParentText !== isInAParentText) {
return isInAParentText;
} else {
return parentContext;
}
}

// This object is used to lazily reuse the ID of the first generated node, or assign one.
// This is very specific to DOM where we can't assign an ID to.
export type SuspenseBoundaryID = number;
Expand Down
5 changes: 5 additions & 0 deletions packages/react-noop-renderer/src/ReactNoopServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ const ReactNoopServer = ReactFizzServer({
return {state: 'pending', children: []};
},

getChildFormatContext(): null {
return null;
},

pushTextInstance(target: Array<Uint8Array>, text: string): void {
const textInstance: TextInstance = {
text,
Expand Down Expand Up @@ -236,6 +240,7 @@ function render(children: React$Element<any>, options?: Options): Destination {
children,
destination,
null,
null,
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
Expand Down
Loading

0 comments on commit 38a1aed

Please sign in to comment.