-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Flight] Integrate Blocks into Flight #18371
Changes from all commits
cc4e388
a5095fd
7f2ecd8
498af84
874eab1
19435d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,18 +7,25 @@ | |
* @flow | ||
*/ | ||
|
||
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; | ||
import type {BlockComponent, BlockRenderFunction} from 'react/src/ReactBlock'; | ||
import type {LazyComponent} from 'react/src/ReactLazy'; | ||
|
||
// import type { | ||
// ModuleReference, | ||
// ModuleMetaData, | ||
// } from './ReactFlightClientHostConfig'; | ||
import type { | ||
ModuleReference, | ||
ModuleMetaData, | ||
} from './ReactFlightClientHostConfig'; | ||
|
||
// import { | ||
// resolveModuleReference, | ||
// preloadModule, | ||
// requireModule, | ||
// } from './ReactFlightClientHostConfig'; | ||
import { | ||
resolveModuleReference, | ||
preloadModule, | ||
requireModule, | ||
} from './ReactFlightClientHostConfig'; | ||
|
||
import { | ||
REACT_LAZY_TYPE, | ||
REACT_BLOCK_TYPE, | ||
REACT_ELEMENT_TYPE, | ||
} from 'shared/ReactSymbols'; | ||
|
||
export type ReactModelRoot<T> = {| | ||
model: T, | ||
|
@@ -32,40 +39,43 @@ export type JSONValue = | |
| {[key: string]: JSONValue} | ||
| Array<JSONValue>; | ||
|
||
const isArray = Array.isArray; | ||
|
||
const PENDING = 0; | ||
const RESOLVED = 1; | ||
const ERRORED = 2; | ||
|
||
const CHUNK_TYPE = Symbol('flight.chunk'); | ||
|
||
type PendingChunk = {| | ||
$$typeof: Symbol, | ||
status: 0, | ||
value: Promise<void>, | ||
resolve: () => void, | ||
|}; | ||
type ResolvedChunk = {| | ||
type ResolvedChunk<T> = {| | ||
$$typeof: Symbol, | ||
status: 1, | ||
value: mixed, | ||
value: T, | ||
resolve: null, | ||
|}; | ||
type ErroredChunk = {| | ||
$$typeof: Symbol, | ||
status: 2, | ||
value: Error, | ||
resolve: null, | ||
|}; | ||
type Chunk = PendingChunk | ResolvedChunk | ErroredChunk; | ||
type Chunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk; | ||
|
||
export type Response = { | ||
partialRow: string, | ||
modelRoot: ReactModelRoot<any>, | ||
chunks: Map<number, Chunk>, | ||
chunks: Map<number, Chunk<any>>, | ||
}; | ||
|
||
export function createResponse(): Response { | ||
let modelRoot: ReactModelRoot<any> = ({}: any); | ||
let rootChunk: Chunk = createPendingChunk(); | ||
let rootChunk: Chunk<any> = createPendingChunk(); | ||
definePendingProperty(modelRoot, 'model', rootChunk); | ||
let chunks: Map<number, Chunk> = new Map(); | ||
let chunks: Map<number, Chunk<any>> = new Map(); | ||
chunks.set(0, rootChunk); | ||
let response = { | ||
partialRow: '', | ||
|
@@ -79,6 +89,7 @@ function createPendingChunk(): PendingChunk { | |
let resolve: () => void = (null: any); | ||
let promise = new Promise(r => (resolve = r)); | ||
return { | ||
$$typeof: CHUNK_TYPE, | ||
status: PENDING, | ||
value: promise, | ||
resolve: resolve, | ||
|
@@ -87,13 +98,14 @@ function createPendingChunk(): PendingChunk { | |
|
||
function createErrorChunk(error: Error): ErroredChunk { | ||
return { | ||
$$typeof: CHUNK_TYPE, | ||
status: ERRORED, | ||
value: error, | ||
resolve: null, | ||
}; | ||
} | ||
|
||
function triggerErrorOnChunk(chunk: Chunk, error: Error): void { | ||
function triggerErrorOnChunk<T>(chunk: Chunk<T>, error: Error): void { | ||
if (chunk.status !== PENDING) { | ||
// We already resolved. We didn't expect to see this. | ||
return; | ||
|
@@ -106,21 +118,22 @@ function triggerErrorOnChunk(chunk: Chunk, error: Error): void { | |
resolve(); | ||
} | ||
|
||
function createResolvedChunk(value: mixed): ResolvedChunk { | ||
function createResolvedChunk<T>(value: T): ResolvedChunk<T> { | ||
return { | ||
$$typeof: CHUNK_TYPE, | ||
status: RESOLVED, | ||
value: value, | ||
resolve: null, | ||
}; | ||
} | ||
|
||
function resolveChunk(chunk: Chunk, value: mixed): void { | ||
function resolveChunk<T>(chunk: Chunk<T>, value: T): void { | ||
if (chunk.status !== PENDING) { | ||
// We already resolved. We didn't expect to see this. | ||
return; | ||
} | ||
let resolve = chunk.resolve; | ||
let resolvedChunk: ResolvedChunk = (chunk: any); | ||
let resolvedChunk: ResolvedChunk<T> = (chunk: any); | ||
resolvedChunk.status = RESOLVED; | ||
resolvedChunk.value = value; | ||
resolvedChunk.resolve = null; | ||
|
@@ -138,10 +151,23 @@ export function reportGlobalError(response: Response, error: Error): void { | |
}); | ||
} | ||
|
||
function definePendingProperty( | ||
function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T { | ||
if ((maybeChunk: any).$$typeof !== CHUNK_TYPE) { | ||
// $FlowFixMe | ||
return maybeChunk; | ||
} | ||
let chunk: Chunk<T> = (maybeChunk: any); | ||
if (chunk.status === RESOLVED) { | ||
return chunk.value; | ||
} else { | ||
throw chunk.value; | ||
} | ||
} | ||
|
||
function definePendingProperty<T>( | ||
object: Object, | ||
key: string, | ||
chunk: Chunk, | ||
chunk: Chunk<T>, | ||
): void { | ||
Object.defineProperty(object, key, { | ||
configurable: false, | ||
|
@@ -197,6 +223,55 @@ function createElement(type, key, props): React$Element<any> { | |
return element; | ||
} | ||
|
||
type UninitializedBlockPayload<Data> = [ | ||
mixed, | ||
ModuleMetaData | Chunk<ModuleMetaData>, | ||
Data | Chunk<Data>, | ||
]; | ||
|
||
type Thenable<T> = { | ||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): Thenable<any>, | ||
}; | ||
|
||
function initializeBlock<Props, Data>( | ||
tuple: UninitializedBlockPayload<Data>, | ||
): BlockComponent<Props, Data> { | ||
// Require module first and then data. The ordering matters. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we test correct ordering? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. Yea. We could test a side-effect in the require somehow while data is still blocked. There's no module function that runs in the tests atm though so needs some infra or hook into the mocks somehow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also don't have any async tests yet for noop which is where we should be adding tests like this. |
||
let moduleMetaData: ModuleMetaData = readMaybeChunk(tuple[1]); | ||
let moduleReference: ModuleReference< | ||
BlockRenderFunction<Props, Data>, | ||
> = resolveModuleReference(moduleMetaData); | ||
// TODO: Do this earlier, as the chunk is resolved. | ||
preloadModule(moduleReference); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment before implementation of
but retvalue is never used. Is it outdated? Also, is it completely unobservable? I deleted this and nothing failed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment is outdated. It's not unobservable because of this: https://github.com/facebook/react/blob/master/packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js#L60-L61 Lots more tests needs to be written at some point. None of the tests cover that chunks are being loaded. |
||
|
||
let moduleExport = requireModule(moduleReference); | ||
|
||
// The ordering here is important because this call might suspend. | ||
// We don't want that to prevent the module graph for being initialized. | ||
let data: Data = readMaybeChunk(tuple[2]); | ||
|
||
return { | ||
$$typeof: REACT_BLOCK_TYPE, | ||
_status: -1, | ||
_data: data, | ||
_render: moduleExport, | ||
}; | ||
} | ||
|
||
function createLazyBlock<Props, Data>( | ||
tuple: UninitializedBlockPayload<Data>, | ||
): LazyComponent<BlockComponent<Props, Data>, UninitializedBlockPayload<Data>> { | ||
let lazyType: LazyComponent< | ||
BlockComponent<Props, Data>, | ||
UninitializedBlockPayload<Data>, | ||
> = { | ||
$$typeof: REACT_LAZY_TYPE, | ||
_payload: tuple, | ||
_init: initializeBlock, | ||
}; | ||
return lazyType; | ||
} | ||
|
||
export function parseModelFromJSON( | ||
response: Response, | ||
targetObj: Object, | ||
|
@@ -217,20 +292,26 @@ export function parseModelFromJSON( | |
if (!chunk) { | ||
chunk = createPendingChunk(); | ||
chunks.set(id, chunk); | ||
} else if (chunk.status === RESOLVED) { | ||
return chunk.value; | ||
} | ||
definePendingProperty(targetObj, key, chunk); | ||
return undefined; | ||
return chunk; | ||
} | ||
} | ||
if (value === '@') { | ||
return REACT_BLOCK_TYPE; | ||
} | ||
} | ||
if (isArray(value)) { | ||
if (typeof value === 'object' && value !== null) { | ||
let tuple: [mixed, mixed, mixed, mixed] = (value: any); | ||
if (tuple[0] === REACT_ELEMENT_TYPE) { | ||
// TODO: Consider having React just directly accept these arrays as elements. | ||
// Or even change the ReactElement type to be an array. | ||
return createElement(tuple[1], tuple[2], tuple[3]); | ||
switch (tuple[0]) { | ||
case REACT_ELEMENT_TYPE: { | ||
// TODO: Consider having React just directly accept these arrays as elements. | ||
// Or even change the ReactElement type to be an array. | ||
return createElement(tuple[1], tuple[2], tuple[3]); | ||
} | ||
case REACT_BLOCK_TYPE: { | ||
// TODO: Consider having React just directly accept these arrays as blocks. | ||
return createLazyBlock((tuple: any)); | ||
} | ||
} | ||
} | ||
return value; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,11 @@ | |
|
||
'use strict'; | ||
|
||
const ReactFeatureFlags = require('shared/ReactFeatureFlags'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @acdlite Did you say we need to put these inline now? I don't remember why. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I copy-pasted this from elsewhere so if we do, we should codemod the rest of them too. |
||
|
||
let act; | ||
let React; | ||
let ReactNoop; | ||
let ReactNoopFlightServer; | ||
let ReactNoopFlightClient; | ||
|
||
|
@@ -19,24 +23,78 @@ describe('ReactFlight', () => { | |
jest.resetModules(); | ||
|
||
React = require('react'); | ||
ReactNoop = require('react-noop-renderer'); | ||
ReactNoopFlightServer = require('react-noop-renderer/flight-server'); | ||
ReactNoopFlightClient = require('react-noop-renderer/flight-client'); | ||
act = ReactNoop.act; | ||
}); | ||
|
||
it('can resolve a model', () => { | ||
function block(query, render) { | ||
return function(...args) { | ||
let curriedQuery = () => { | ||
return query(...args); | ||
}; | ||
return [Symbol.for('react.server.block'), render, curriedQuery]; | ||
}; | ||
} | ||
|
||
it('can render a server component', () => { | ||
function Bar({text}) { | ||
return text.toUpperCase(); | ||
} | ||
function Foo() { | ||
return { | ||
bar: [<Bar text="a" />, <Bar text="b" />], | ||
bar: ( | ||
<div> | ||
<Bar text="a" />, <Bar text="b" /> | ||
</div> | ||
), | ||
}; | ||
} | ||
let transport = ReactNoopFlightServer.render({ | ||
foo: <Foo />, | ||
}); | ||
let root = ReactNoopFlightClient.read(transport); | ||
let model = root.model; | ||
expect(model).toEqual({foo: {bar: ['A', 'B']}}); | ||
expect(model).toEqual({ | ||
foo: { | ||
bar: ( | ||
<div> | ||
{'A'} | ||
{', '} | ||
{'B'} | ||
</div> | ||
), | ||
}, | ||
}); | ||
}); | ||
|
||
if (ReactFeatureFlags.enableBlocksAPI) { | ||
it('can transfer a Block to the client and render there', () => { | ||
function Query(firstName, lastName) { | ||
return {name: firstName + ' ' + lastName}; | ||
} | ||
function User(props, data) { | ||
return ( | ||
<span> | ||
{props.greeting}, {data.name} | ||
</span> | ||
); | ||
} | ||
let loadUser = block(Query, User); | ||
let model = { | ||
User: loadUser('Seb', 'Smith'), | ||
}; | ||
|
||
let transport = ReactNoopFlightServer.render(model); | ||
let root = ReactNoopFlightClient.read(transport); | ||
|
||
act(() => { | ||
let UserClient = root.model.User; | ||
ReactNoop.render(<UserClient greeting="Hello" />); | ||
}); | ||
|
||
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>); | ||
}); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've noticed
maybeChunk
is literally theUser
function in the Relay test. Is this intentional? We're missing some mocking that represents the module transport there?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't intentional at first but I did notice it while writing the tests and decided to keep it. I think we should do some mocking here but we don't have it wired up yet in www so we're not sure what we're simulating yet. Once we have it set up we can simulate that here in the mocks.
Also, the tests are far from sufficient yet. Needs more async stuff. I need to write more but don't want to block on it.