Flip the arguments of Blocks and make the query optional (#18374)

* Flip the arguments of Blocks and make the query optional

* Rename Query to Load
This commit is contained in:
Sebastian Markbåge 2020-03-24 10:58:23 -07:00 committed by GitHub
parent fc7835c657
commit a317bd033f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 116 additions and 44 deletions

View File

@ -152,7 +152,7 @@ export function reportGlobalError(response: Response, error: Error): void {
} }
function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T { function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
if ((maybeChunk: any).$$typeof !== CHUNK_TYPE) { if (maybeChunk == null || (maybeChunk: any).$$typeof !== CHUNK_TYPE) {
// $FlowFixMe // $FlowFixMe
return maybeChunk; return maybeChunk;
} }

View File

@ -29,12 +29,15 @@ describe('ReactFlight', () => {
act = ReactNoop.act; act = ReactNoop.act;
}); });
function block(query, render) { function block(render, load) {
return function(...args) { return function(...args) {
let curriedQuery = () => { if (load === undefined) {
return query(...args); 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) { if (ReactFeatureFlags.enableBlocksAPI) {
it('can transfer a Block to the client and render there', () => { it('can transfer a Block to the client and render there, without data', () => {
function Query(firstName, lastName) { function User(props, data) {
return (
<span>
{props.greeting} {typeof data}
</span>
);
}
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(<UserClient greeting="Hello" />);
});
expect(ReactNoop).toMatchRenderedOutput(<span>Hello undefined</span>);
});
it('can transfer a Block to the client and render there, with data', () => {
function load(firstName, lastName) {
return {name: firstName + ' ' + lastName}; return {name: firstName + ' ' + lastName};
} }
function User(props, data) { function User(props, data) {
@ -81,7 +108,7 @@ describe('ReactFlight', () => {
</span> </span>
); );
} }
let loadUser = block(Query, User); let loadUser = block(User, load);
let model = { let model = {
User: loadUser('Seb', 'Smith'), User: loadUser('Seb', 'Smith'),
}; };

View File

@ -44,12 +44,15 @@ describe('ReactFlightDOMRelay', () => {
return model; return model;
} }
function block(query, render) { function block(render, load) {
return function(...args) { return function(...args) {
let curriedQuery = () => { if (load === undefined) {
return query(...args); 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', () => { it.experimental('can transfer a Block to the client and render there', () => {
function Query(firstName, lastName) { function load(firstName, lastName) {
return {name: firstName + ' ' + lastName}; return {name: firstName + ' ' + lastName};
} }
function User(props, data) { function User(props, data) {
@ -99,7 +102,7 @@ describe('ReactFlightDOMRelay', () => {
</span> </span>
); );
} }
let loadUser = block(Query, User); let loadUser = block(User, load);
let model = { let model = {
User: loadUser('Seb', 'Smith'), User: loadUser('Seb', 'Smith'),
}; };

View File

@ -62,7 +62,7 @@ describe('ReactFlightDOM', () => {
}; };
} }
function block(query, render) { function block(render, load) {
let idx = webpackModuleIdx++; let idx = webpackModuleIdx++;
webpackModules[idx] = { webpackModules[idx] = {
d: render, d: render,
@ -73,10 +73,13 @@ describe('ReactFlightDOM', () => {
name: 'd', name: 'd',
}; };
return function(...args) { return function(...args) {
let curriedQuery = () => { if (load === undefined) {
return query(...args); 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); reject(e);
}; };
}); });
function Query() { function load() {
if (promise) { if (promise) {
throw promise; throw promise;
} }
@ -300,8 +303,8 @@ describe('ReactFlightDOM', () => {
function DelayedText({children}, data) { function DelayedText({children}, data) {
return <Text>{children}</Text>; return <Text>{children}</Text>;
} }
let _block = block(Query, DelayedText); let loadBlock = block(DelayedText, load);
return [_block(), _resolve, _reject]; return [loadBlock(), _resolve, _reject];
} }
const [FriendsModel, resolveFriendsModel] = makeDelayedText(); const [FriendsModel, resolveFriendsModel] = makeDelayedText();

View File

@ -49,8 +49,30 @@ describe('ReactBlocks', () => {
}; };
}); });
it.experimental('renders a simple component', () => {
function User(props, data) {
return <div>{typeof data}</div>;
}
function App({Component}) {
return (
<Suspense fallback={'Loading...'}>
<Component name="Name" />
</Suspense>
);
}
let loadUser = block(User);
ReactNoop.act(() => {
ReactNoop.render(<App Component={loadUser()} />);
});
expect(ReactNoop).toMatchRenderedOutput(<div>undefined</div>);
});
it.experimental('prints the name of the render function in warnings', () => { it.experimental('prints the name of the render function in warnings', () => {
function Query(firstName) { function load(firstName) {
return { return {
name: firstName, name: firstName,
}; };
@ -69,7 +91,7 @@ describe('ReactBlocks', () => {
); );
} }
let loadUser = block(Query, User); let loadUser = block(User, load);
expect(() => { expect(() => {
ReactNoop.act(() => { ReactNoop.act(() => {
@ -86,8 +108,8 @@ describe('ReactBlocks', () => {
); );
}); });
it.experimental('renders a component with a suspending query', async () => { it.experimental('renders a component with a suspending load', async () => {
function Query(id) { function load(id) {
return { return {
id: id, id: id,
name: readString('Sebastian'), name: readString('Sebastian'),
@ -102,7 +124,7 @@ describe('ReactBlocks', () => {
); );
} }
let loadUser = block(Query, Render); let loadUser = block(Render, load);
function App({User}) { function App({User}) {
return ( return (
@ -128,7 +150,7 @@ describe('ReactBlocks', () => {
it.experimental( it.experimental(
'does not support a lazy wrapper around a chunk', 'does not support a lazy wrapper around a chunk',
async () => { async () => {
function Query(id) { function load(id) {
return { return {
id: id, id: id,
name: readString('Sebastian'), name: readString('Sebastian'),
@ -143,7 +165,7 @@ describe('ReactBlocks', () => {
); );
} }
let loadUser = block(Query, Render); let loadUser = block(Render, load);
function App({User}) { function App({User}) {
return ( return (
@ -187,7 +209,7 @@ describe('ReactBlocks', () => {
it.experimental( it.experimental(
'can receive updated data for the same component', 'can receive updated data for the same component',
async () => { async () => {
function Query(firstName) { function load(firstName) {
return { return {
name: firstName, name: firstName,
}; };
@ -203,7 +225,7 @@ describe('ReactBlocks', () => {
); );
} }
let loadUser = block(Query, Render); let loadUser = block(Render, load);
function App({User}) { function App({User}) {
return ( return (

View File

@ -191,11 +191,11 @@ export function resolveModelToJSON(
} }
} }
case '2': { case '2': {
// Query // Load function
let query: () => ReactModel = (value: any); let load: () => ReactModel = (value: any);
try { try {
// Attempt to resolve the query. // Attempt to resolve the data.
return query(); return load();
} catch (x) { } catch (x) {
if ( if (
typeof x === 'object' && typeof x === 'object' &&
@ -204,12 +204,12 @@ export function resolveModelToJSON(
) { ) {
// Something suspended, we'll need to create a new segment and resolve it later. // Something suspended, we'll need to create a new segment and resolve it later.
request.pendingChunks++; request.pendingChunks++;
let newSegment = createSegment(request, query); let newSegment = createSegment(request, load);
let ping = newSegment.ping; let ping = newSegment.ping;
x.then(ping, ping); x.then(ping, ping);
return serializeIDRef(newSegment.id); return serializeIDRef(newSegment.id);
} else { } 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++; request.pendingChunks++;
let errorId = request.nextChunkId++; let errorId = request.nextChunkId++;
emitErrorChunk(request, errorId, x); emitErrorChunk(request, errorId, x);

View File

@ -16,14 +16,14 @@ import {
REACT_FORWARD_REF_TYPE, REACT_FORWARD_REF_TYPE,
} from 'shared/ReactSymbols'; } from 'shared/ReactSymbols';
type BlockQueryFunction<Args: Iterable<any>, Data> = (...args: Args) => Data; type BlockLoadFunction<Args: Iterable<any>, Data> = (...args: Args) => Data;
export type BlockRenderFunction<Props, Data> = ( export type BlockRenderFunction<Props, Data> = (
props: Props, props: Props,
data: Data, data: Data,
) => React$Node; ) => React$Node;
type Payload<Props, Args: Iterable<any>, Data> = { type Payload<Props, Args: Iterable<any>, Data> = {
query: BlockQueryFunction<Args, Data>, load: BlockLoadFunction<Args, Data>,
args: Args, args: Args,
render: BlockRenderFunction<Props, Data>, render: BlockRenderFunction<Props, Data>,
}; };
@ -44,20 +44,20 @@ function lazyInitializer<Props, Args: Iterable<any>, Data>(
): BlockComponent<Props, Data> { ): BlockComponent<Props, Data> {
return { return {
$$typeof: REACT_BLOCK_TYPE, $$typeof: REACT_BLOCK_TYPE,
_data: payload.query.apply(null, payload.args), _data: payload.load.apply(null, payload.args),
_render: payload.render, _render: payload.render,
}; };
} }
export function block<Args: Iterable<any>, Props, Data>( export function block<Args: Iterable<any>, Props, Data>(
query: BlockQueryFunction<Args, Data>,
render: BlockRenderFunction<Props, Data>, render: BlockRenderFunction<Props, Data>,
load?: BlockLoadFunction<Args, Data>,
): (...args: Args) => Block<Props> { ): (...args: Args) => Block<Props> {
if (__DEV__) { if (__DEV__) {
if (typeof query !== 'function') { if (load !== undefined && typeof load !== 'function') {
console.error( console.error(
'Blocks require a query function but was given %s.', 'Blocks require a load function, if provided, but was given %s.',
query === null ? 'null' : typeof query, load === null ? 'null' : typeof load,
); );
} }
if (render != null && render.$$typeof === REACT_MEMO_TYPE) { if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
@ -97,11 +97,28 @@ export function block<Args: Iterable<any>, Props, Data>(
} }
} }
if (load === undefined) {
return function(): Block<Props> {
let blockComponent: BlockComponent<Props, void> = {
$$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<Props> { return function(): Block<Props> {
let args: Args = arguments; let args: Args = arguments;
let payload: Payload<Props, Args, Data> = { let payload: Payload<Props, Args, Data> = {
query: query, load: loadFn,
args: args, args: args,
render: render, render: render,
}; };