diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 2093ac7a49..f4e9bfb006 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -152,7 +152,7 @@ export function reportGlobalError(response: Response, error: Error): void { } function readMaybeChunk(maybeChunk: Chunk | T): T { - if ((maybeChunk: any).$$typeof !== CHUNK_TYPE) { + if (maybeChunk == null || (maybeChunk: any).$$typeof !== CHUNK_TYPE) { // $FlowFixMe return maybeChunk; } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 7b981aa040..446a774f70 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -29,12 +29,15 @@ describe('ReactFlight', () => { act = ReactNoop.act; }); - function block(query, render) { + function block(render, load) { return function(...args) { - let curriedQuery = () => { - return query(...args); + if (load === undefined) { + return [Symbol.for('react.server.block'), render]; + } + let curriedLoad = () => { + return load(...args); }; - return [Symbol.for('react.server.block'), render, curriedQuery]; + return [Symbol.for('react.server.block'), render, curriedLoad]; }; } @@ -70,8 +73,32 @@ describe('ReactFlight', () => { }); if (ReactFeatureFlags.enableBlocksAPI) { - it('can transfer a Block to the client and render there', () => { - function Query(firstName, lastName) { + it('can transfer a Block to the client and render there, without data', () => { + function User(props, data) { + return ( + + {props.greeting} {typeof data} + + ); + } + let loadUser = block(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(); + }); + + expect(ReactNoop).toMatchRenderedOutput(Hello undefined); + }); + + it('can transfer a Block to the client and render there, with data', () => { + function load(firstName, lastName) { return {name: firstName + ' ' + lastName}; } function User(props, data) { @@ -81,7 +108,7 @@ describe('ReactFlight', () => { ); } - let loadUser = block(Query, User); + let loadUser = block(User, load); let model = { User: loadUser('Seb', 'Smith'), }; diff --git a/packages/react-flight-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js b/packages/react-flight-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js index 046f998742..c157e3741e 100644 --- a/packages/react-flight-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js +++ b/packages/react-flight-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js @@ -44,12 +44,15 @@ describe('ReactFlightDOMRelay', () => { return model; } - function block(query, render) { + function block(render, load) { return function(...args) { - let curriedQuery = () => { - return query(...args); + if (load === undefined) { + return [Symbol.for('react.server.block'), render]; + } + let curriedLoad = () => { + return load(...args); }; - return [Symbol.for('react.server.block'), render, curriedQuery]; + return [Symbol.for('react.server.block'), render, curriedLoad]; }; } @@ -89,7 +92,7 @@ describe('ReactFlightDOMRelay', () => { }); it.experimental('can transfer a Block to the client and render there', () => { - function Query(firstName, lastName) { + function load(firstName, lastName) { return {name: firstName + ' ' + lastName}; } function User(props, data) { @@ -99,7 +102,7 @@ describe('ReactFlightDOMRelay', () => { ); } - let loadUser = block(Query, User); + let loadUser = block(User, load); let model = { User: loadUser('Seb', 'Smith'), }; diff --git a/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 108a044eda..468545277e 100644 --- a/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -62,7 +62,7 @@ describe('ReactFlightDOM', () => { }; } - function block(query, render) { + function block(render, load) { let idx = webpackModuleIdx++; webpackModules[idx] = { d: render, @@ -73,10 +73,13 @@ describe('ReactFlightDOM', () => { name: 'd', }; return function(...args) { - let curriedQuery = () => { - return query(...args); + if (load === undefined) { + return [Symbol.for('react.server.block'), render]; + } + let curriedLoad = () => { + return load(...args); }; - return [Symbol.for('react.server.block'), 'path/' + idx, curriedQuery]; + return [Symbol.for('react.server.block'), 'path/' + idx, curriedLoad]; }; } @@ -288,7 +291,7 @@ describe('ReactFlightDOM', () => { reject(e); }; }); - function Query() { + function load() { if (promise) { throw promise; } @@ -300,8 +303,8 @@ describe('ReactFlightDOM', () => { function DelayedText({children}, data) { return {children}; } - let _block = block(Query, DelayedText); - return [_block(), _resolve, _reject]; + let loadBlock = block(DelayedText, load); + return [loadBlock(), _resolve, _reject]; } const [FriendsModel, resolveFriendsModel] = makeDelayedText(); diff --git a/packages/react-reconciler/src/__tests__/ReactBlocks-test.js b/packages/react-reconciler/src/__tests__/ReactBlocks-test.js index 31c6cbd174..19ab543ff4 100644 --- a/packages/react-reconciler/src/__tests__/ReactBlocks-test.js +++ b/packages/react-reconciler/src/__tests__/ReactBlocks-test.js @@ -49,8 +49,30 @@ describe('ReactBlocks', () => { }; }); + it.experimental('renders a simple component', () => { + function User(props, data) { + return
{typeof data}
; + } + + function App({Component}) { + return ( + + + + ); + } + + let loadUser = block(User); + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(ReactNoop).toMatchRenderedOutput(
undefined
); + }); + it.experimental('prints the name of the render function in warnings', () => { - function Query(firstName) { + function load(firstName) { return { name: firstName, }; @@ -69,7 +91,7 @@ describe('ReactBlocks', () => { ); } - let loadUser = block(Query, User); + let loadUser = block(User, load); expect(() => { ReactNoop.act(() => { @@ -86,8 +108,8 @@ describe('ReactBlocks', () => { ); }); - it.experimental('renders a component with a suspending query', async () => { - function Query(id) { + it.experimental('renders a component with a suspending load', async () => { + function load(id) { return { id: id, name: readString('Sebastian'), @@ -102,7 +124,7 @@ describe('ReactBlocks', () => { ); } - let loadUser = block(Query, Render); + let loadUser = block(Render, load); function App({User}) { return ( @@ -128,7 +150,7 @@ describe('ReactBlocks', () => { it.experimental( 'does not support a lazy wrapper around a chunk', async () => { - function Query(id) { + function load(id) { return { id: id, name: readString('Sebastian'), @@ -143,7 +165,7 @@ describe('ReactBlocks', () => { ); } - let loadUser = block(Query, Render); + let loadUser = block(Render, load); function App({User}) { return ( @@ -187,7 +209,7 @@ describe('ReactBlocks', () => { it.experimental( 'can receive updated data for the same component', async () => { - function Query(firstName) { + function load(firstName) { return { name: firstName, }; @@ -203,7 +225,7 @@ describe('ReactBlocks', () => { ); } - let loadUser = block(Query, Render); + let loadUser = block(Render, load); function App({User}) { return ( diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 78c2fc1e9d..393c3ce4e1 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -191,11 +191,11 @@ export function resolveModelToJSON( } } case '2': { - // Query - let query: () => ReactModel = (value: any); + // Load function + let load: () => ReactModel = (value: any); try { - // Attempt to resolve the query. - return query(); + // Attempt to resolve the data. + return load(); } catch (x) { if ( typeof x === 'object' && @@ -204,12 +204,12 @@ export function resolveModelToJSON( ) { // Something suspended, we'll need to create a new segment and resolve it later. request.pendingChunks++; - let newSegment = createSegment(request, query); + let newSegment = createSegment(request, load); let ping = newSegment.ping; x.then(ping, ping); return serializeIDRef(newSegment.id); } else { - // This query failed, encode the error as a separate row and reference that. + // This load failed, encode the error as a separate row and reference that. request.pendingChunks++; let errorId = request.nextChunkId++; emitErrorChunk(request, errorId, x); diff --git a/packages/react/src/ReactBlock.js b/packages/react/src/ReactBlock.js index 70a8761995..17b5769a8a 100644 --- a/packages/react/src/ReactBlock.js +++ b/packages/react/src/ReactBlock.js @@ -16,14 +16,14 @@ import { REACT_FORWARD_REF_TYPE, } from 'shared/ReactSymbols'; -type BlockQueryFunction, Data> = (...args: Args) => Data; +type BlockLoadFunction, Data> = (...args: Args) => Data; export type BlockRenderFunction = ( props: Props, data: Data, ) => React$Node; type Payload, Data> = { - query: BlockQueryFunction, + load: BlockLoadFunction, args: Args, render: BlockRenderFunction, }; @@ -44,20 +44,20 @@ function lazyInitializer, Data>( ): BlockComponent { return { $$typeof: REACT_BLOCK_TYPE, - _data: payload.query.apply(null, payload.args), + _data: payload.load.apply(null, payload.args), _render: payload.render, }; } export function block, Props, Data>( - query: BlockQueryFunction, render: BlockRenderFunction, + load?: BlockLoadFunction, ): (...args: Args) => Block { if (__DEV__) { - if (typeof query !== 'function') { + if (load !== undefined && typeof load !== 'function') { console.error( - 'Blocks require a query function but was given %s.', - query === null ? 'null' : typeof query, + 'Blocks require a load function, if provided, but was given %s.', + load === null ? 'null' : typeof load, ); } if (render != null && render.$$typeof === REACT_MEMO_TYPE) { @@ -97,11 +97,28 @@ export function block, Props, Data>( } } + if (load === undefined) { + return function(): Block { + let blockComponent: BlockComponent = { + $$typeof: REACT_BLOCK_TYPE, + _data: undefined, + // $FlowFixMe: Data must be void in this scenario. + _render: render, + }; + + // $FlowFixMe + return blockComponent; + }; + } + + // Trick to let Flow refine this. + let loadFn = load; + return function(): Block { let args: Args = arguments; let payload: Payload = { - query: query, + load: loadFn, args: args, render: render, };