mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
The idea here is that host dispatchers are not bound to renders so we need to be able to dispatch to them at any time. This updates the implementation to chain these dispatchers so that each renderer can respond to the dispatch. Semantically we don't always want every renderer to do this for instance if Fizz handles a float method we don't want Fiber to as well so each dispatcher implementation can decide if it makes sense to forward the call or not. For float methods server disaptchers will handle the call if they can resolve a Request otherwise they will forward. For client dispatchers they will handle the call and always forward. The choice needs to be made for each dispatcher method and may have implications on correct renderer import order. For now we just live with the restriction that if you want to use server and client together (such as renderToString in the browser) you need to import the server renderer after the client renderer.
307 lines
7.9 KiB
JavaScript
307 lines
7.9 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
/**
|
|
* This is a renderer of React that doesn't have a render target output.
|
|
* It is useful to demonstrate the internals of the reconciler in isolation
|
|
* and for testing semantics of reconciliation separate from the host
|
|
* environment.
|
|
*/
|
|
|
|
import type {ReactNodeList} from 'shared/ReactTypes';
|
|
|
|
import ReactFizzServer from 'react-server';
|
|
|
|
type Instance = {
|
|
type: string,
|
|
children: Array<Instance | TextInstance | SuspenseInstance>,
|
|
prop: any,
|
|
hidden: boolean,
|
|
};
|
|
|
|
type TextInstance = {
|
|
text: string,
|
|
hidden: boolean,
|
|
};
|
|
|
|
type SuspenseInstance = {
|
|
state: 'pending' | 'complete' | 'client-render',
|
|
children: Array<Instance | TextInstance | SuspenseInstance>,
|
|
};
|
|
|
|
type Placeholder = {
|
|
parent: Instance | SuspenseInstance,
|
|
index: number,
|
|
};
|
|
|
|
type Segment = {
|
|
children: null | Instance | TextInstance | SuspenseInstance,
|
|
};
|
|
|
|
type Destination = {
|
|
root: null | Instance | TextInstance | SuspenseInstance,
|
|
placeholders: Map<number, Placeholder>,
|
|
segments: Map<number, Segment>,
|
|
stack: Array<Segment | Instance | SuspenseInstance>,
|
|
};
|
|
|
|
type RenderState = null;
|
|
type HoistableState = null;
|
|
|
|
const POP = Buffer.from('/', 'utf8');
|
|
|
|
function write(destination: Destination, buffer: Uint8Array): void {
|
|
const stack = destination.stack;
|
|
if (buffer === POP) {
|
|
stack.pop();
|
|
return;
|
|
}
|
|
// We assume one chunk is one instance.
|
|
const instance = JSON.parse(Buffer.from((buffer: any)).toString('utf8'));
|
|
if (stack.length === 0) {
|
|
destination.root = instance;
|
|
} else {
|
|
const parent = stack[stack.length - 1];
|
|
parent.children.push(instance);
|
|
}
|
|
stack.push(instance);
|
|
}
|
|
|
|
const ReactNoopServer = ReactFizzServer({
|
|
scheduleWork(callback: () => void) {
|
|
callback();
|
|
},
|
|
beginWriting(destination: Destination): void {},
|
|
writeChunk(destination: Destination, buffer: Uint8Array): void {
|
|
write(destination, buffer);
|
|
},
|
|
writeChunkAndReturn(destination: Destination, buffer: Uint8Array): boolean {
|
|
write(destination, buffer);
|
|
return true;
|
|
},
|
|
completeWriting(destination: Destination): void {},
|
|
close(destination: Destination): void {},
|
|
closeWithError(destination: Destination, error: mixed): void {},
|
|
flushBuffered(destination: Destination): void {},
|
|
|
|
getChildFormatContext(): null {
|
|
return null;
|
|
},
|
|
|
|
resetResumableState(): void {},
|
|
completeResumableState(): void {},
|
|
|
|
pushTextInstance(
|
|
target: Array<Uint8Array>,
|
|
text: string,
|
|
renderState: RenderState,
|
|
textEmbedded: boolean,
|
|
): boolean {
|
|
const textInstance: TextInstance = {
|
|
text,
|
|
hidden: false,
|
|
};
|
|
target.push(Buffer.from(JSON.stringify(textInstance), 'utf8'), POP);
|
|
return false;
|
|
},
|
|
pushStartInstance(
|
|
target: Array<Uint8Array>,
|
|
type: string,
|
|
props: Object,
|
|
): ReactNodeList {
|
|
const instance: Instance = {
|
|
type: type,
|
|
children: [],
|
|
prop: props.prop,
|
|
hidden: false,
|
|
};
|
|
target.push(Buffer.from(JSON.stringify(instance), 'utf8'));
|
|
return props.children;
|
|
},
|
|
|
|
pushEndInstance(
|
|
target: Array<Uint8Array>,
|
|
type: string,
|
|
props: Object,
|
|
): void {
|
|
target.push(POP);
|
|
},
|
|
|
|
// This is a noop in ReactNoop
|
|
pushSegmentFinale(
|
|
target: Array<Uint8Array>,
|
|
renderState: RenderState,
|
|
lastPushedText: boolean,
|
|
textEmbedded: boolean,
|
|
): void {},
|
|
|
|
writeCompletedRoot(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
): boolean {
|
|
return true;
|
|
},
|
|
|
|
writePlaceholder(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
id: number,
|
|
): boolean {
|
|
const parent = destination.stack[destination.stack.length - 1];
|
|
destination.placeholders.set(id, {
|
|
parent: parent,
|
|
index: parent.children.length,
|
|
});
|
|
},
|
|
|
|
writeStartCompletedSuspenseBoundary(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
suspenseInstance: SuspenseInstance,
|
|
): boolean {
|
|
suspenseInstance.state = 'complete';
|
|
const parent = destination.stack[destination.stack.length - 1];
|
|
parent.children.push(suspenseInstance);
|
|
destination.stack.push(suspenseInstance);
|
|
},
|
|
writeStartPendingSuspenseBoundary(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
suspenseInstance: SuspenseInstance,
|
|
): boolean {
|
|
suspenseInstance.state = 'pending';
|
|
const parent = destination.stack[destination.stack.length - 1];
|
|
parent.children.push(suspenseInstance);
|
|
destination.stack.push(suspenseInstance);
|
|
},
|
|
writeStartClientRenderedSuspenseBoundary(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
suspenseInstance: SuspenseInstance,
|
|
): boolean {
|
|
suspenseInstance.state = 'client-render';
|
|
const parent = destination.stack[destination.stack.length - 1];
|
|
parent.children.push(suspenseInstance);
|
|
destination.stack.push(suspenseInstance);
|
|
},
|
|
writeEndCompletedSuspenseBoundary(destination: Destination): boolean {
|
|
destination.stack.pop();
|
|
},
|
|
writeEndPendingSuspenseBoundary(destination: Destination): boolean {
|
|
destination.stack.pop();
|
|
},
|
|
writeEndClientRenderedSuspenseBoundary(destination: Destination): boolean {
|
|
destination.stack.pop();
|
|
},
|
|
|
|
writeStartSegment(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
formatContext: null,
|
|
id: number,
|
|
): boolean {
|
|
const segment = {
|
|
children: [],
|
|
};
|
|
destination.segments.set(id, segment);
|
|
if (destination.stack.length > 0) {
|
|
throw new Error('Segments are only expected at the root of the stack.');
|
|
}
|
|
destination.stack.push(segment);
|
|
},
|
|
writeEndSegment(destination: Destination, formatContext: null): boolean {
|
|
destination.stack.pop();
|
|
},
|
|
|
|
writeCompletedSegmentInstruction(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
contentSegmentID: number,
|
|
): boolean {
|
|
const segment = destination.segments.get(contentSegmentID);
|
|
if (!segment) {
|
|
throw new Error('Missing segment.');
|
|
}
|
|
const placeholder = destination.placeholders.get(contentSegmentID);
|
|
if (!placeholder) {
|
|
throw new Error('Missing placeholder.');
|
|
}
|
|
placeholder.parent.children.splice(
|
|
placeholder.index,
|
|
0,
|
|
...segment.children,
|
|
);
|
|
},
|
|
|
|
writeCompletedBoundaryInstruction(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
boundary: SuspenseInstance,
|
|
contentSegmentID: number,
|
|
): boolean {
|
|
const segment = destination.segments.get(contentSegmentID);
|
|
if (!segment) {
|
|
throw new Error('Missing segment.');
|
|
}
|
|
boundary.children = segment.children;
|
|
boundary.state = 'complete';
|
|
},
|
|
|
|
writeClientRenderBoundaryInstruction(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
boundary: SuspenseInstance,
|
|
): boolean {
|
|
boundary.status = 'client-render';
|
|
},
|
|
|
|
writePreamble() {},
|
|
writeHoistables() {},
|
|
writeHoistablesForBoundary() {},
|
|
writePostamble() {},
|
|
hoistHoistables(parent: HoistableState, child: HoistableState) {},
|
|
createHoistableState(): HoistableState {
|
|
return null;
|
|
},
|
|
emitEarlyPreloads() {},
|
|
});
|
|
|
|
type Options = {
|
|
progressiveChunkSize?: number,
|
|
onShellReady?: () => void,
|
|
onAllReady?: () => void,
|
|
onError?: (error: mixed) => ?string,
|
|
};
|
|
|
|
function render(children: React$Element<any>, options?: Options): Destination {
|
|
const destination: Destination = {
|
|
root: null,
|
|
placeholders: new Map(),
|
|
segments: new Map(),
|
|
stack: [],
|
|
abort() {
|
|
ReactNoopServer.abort(request);
|
|
},
|
|
};
|
|
const request = ReactNoopServer.createRequest(
|
|
children,
|
|
null,
|
|
null,
|
|
options ? options.progressiveChunkSize : undefined,
|
|
options ? options.onError : undefined,
|
|
options ? options.onAllReady : undefined,
|
|
options ? options.onShellReady : undefined,
|
|
);
|
|
ReactNoopServer.startWork(request);
|
|
ReactNoopServer.startFlowing(request, destination);
|
|
return destination;
|
|
}
|
|
|
|
export {render};
|