Skip to content

Add Experimental Flight Infrastructure #16398

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

Merged
merged 6 commits into from
Oct 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions fixtures/flight-browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Flight Example</title>
</head>
<body>
<h1>Flight Example</h1>
<div id="container">
<p>
To install React, follow the instructions on
<a href="https://github.com/facebook/react/">GitHub</a>.
</p>
<p>
If you can see this, React is <strong>not</strong> working right.
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
</p>
</div>
<script src="../../build/dist/react.development.js"></script>
<script src="../../build/dist/react-dom.development.js"></script>
<script src="../../build/dist/react-dom-unstable-flight-client.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<script type="text/babel">
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}

let model = {
title: 'Title',
content: {
__html: <HTML />,
Copy link
Member

@josephsavona josephsavona Aug 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking that I'm following what's happening here: "model" values can render components, which are (at least for now) effectively rendered on the server and sent down as an HTML string (this is the "E.g. a host component tree can be flattened into raw HTML." from your description, right?).

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Aug 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea. The HTML component is actually just a Model function. Then that model happens to contains "host" components (leaves) which are flattened into HTML. This will likely need to be much more clever to protect against XSS attacks like #3473 and to allow targeting nested nodes with client components etc.

I could also do return { __html: <div>...</div> } inside the HTML component since it's just a Model function and can return arbitrary data structures.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nice thing about this pattern is that if you have a tree of stateless functional components, the same code can be one shot rendered like this, or client rendered.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it's just a Model function and can return arbitrary data structures

Can you clarify in your current mental model: Is a Model function actually a component (i.e. can I use hooks in it), or are you using <HTML /> here for sugar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Model function can not use hooks. It's not a client-side component so it cannot have effects or state. (We could potentially let it have access to useContext though.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it suspend? If not, how can it fetch any async data?

I know this is just a plumbing PR, but it would be super helpful if you could include a small end-to-end example 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s one possible solution but that’s really not decided. It could be async functions, it could be something else. :)

}
};

let stream = ReactFlightDOMClient.renderToReadableStream(model);
let response = new Response(stream, {
headers: {'Content-Type': 'text/html'},
});
display(response);

async function display(responseToDisplay) {
let blob = await responseToDisplay.blob();
let url = URL.createObjectURL(blob);
let response = await fetch(url);
let body = await response.body;

let reader = body.getReader();
let charsReceived = 0;
let decoder = new TextDecoder();

let json = '';
reader.read().then(function processChunk({ done, value }) {
if (done) {
renderResult(json);
return;
}
json += decoder.decode(value);
Copy link
Collaborator

@sophiebits sophiebits Aug 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs a second argument {stream: true} in order to properly handle the case that a chunk boundary splits a codepoint, and then in the if above, add

json += decoder.decode();

to finish. https://encoding.spec.whatwg.org/#example-end-of-stream

return reader.read().then(processChunk);
});
}

function Shell({ model }) {
return <div>
<h1>{model.title}</h1>
<div dangerouslySetInnerHTML={model.content} />
</div>;
}

function renderResult(json) {
let model = JSON.parse(json);
let container = document.getElementById('container');
ReactDOM.render(<Shell model={model} />, container);
}
</script>
</body>
</html>
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-flight-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-flight-client.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-flight-client.development.js');
}
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-flight-server.browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-flight-server.browser.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-flight-server.browser.development.js');
}
3 changes: 3 additions & 0 deletions packages/react-dom/npm/unstable-flight-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = require('./unstable-flight-server.node');
7 changes: 7 additions & 0 deletions packages/react-dom/npm/unstable-flight-server.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-dom-unstable-flight-server.node.production.min.js');
} else {
module.exports = require('./cjs/react-dom-unstable-flight-server.node.development.js');
}
6 changes: 5 additions & 1 deletion packages/react-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,17 @@
"unstable-fizz.js",
"unstable-fizz.browser.js",
"unstable-fizz.node.js",
"unstable-flight-server.js",
"unstable-flight-server.browser.js",
"unstable-flight-server.node.js",
"unstable-native-dependencies.js",
"cjs/",
"umd/"
],
"browser": {
"./server.js": "./server.browser.js",
"./unstable-fizz.js": "./unstable-fizz.browser.js"
"./unstable-fizz.js": "./unstable-fizz.browser.js",
"./unstable-flight-server.js": "./unstable-flight-server.browser.js"
},
"browserify": {
"transform": [
Expand Down
34 changes: 34 additions & 0 deletions packages/react-dom/src/client/flight/ReactFlightDOMClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {ReactModel} from 'react-flight/src/ReactFlightClient';

import {
createRequest,
startWork,
startFlowing,
} from 'react-flight/inline.dom-browser';

function renderToReadableStream(model: ReactModel): ReadableStream {
let request;
return new ReadableStream({
start(controller) {
request = createRequest(model, controller);
startWork(request);
},
pull(controller) {
startFlowing(request, controller.desiredSize);
},
cancel(reason) {},
});
}

export default {
renderToReadableStream,
};
50 changes: 50 additions & 0 deletions packages/react-dom/src/client/flight/ReactFlightDOMHostConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export type Destination = ReadableStreamController;

export function scheduleWork(callback: () => void) {
callback();
}

export function flushBuffered(destination: Destination) {
// WHATWG Streams do not yet have a way to flush the underlying
// transform streams. https://github.com/whatwg/streams/issues/960
}

export function beginWriting(destination: Destination) {}

export function writeChunk(destination: Destination, buffer: Uint8Array) {
destination.enqueue(buffer);
}

export function completeWriting(destination: Destination) {}

export function close(destination: Destination) {
destination.close();
}

const textEncoder = new TextEncoder();

export function convertStringToBuffer(content: string): Uint8Array {
return textEncoder.encode(content);
}

export function formatChunkAsString(type: string, props: Object): string {
let str = '<' + type + '>';
if (typeof props.children === 'string') {
str += props.children;
}
str += '</' + type + '>';
return str;
}

export function formatChunk(type: string, props: Object): Uint8Array {
return convertStringToBuffer(formatChunkAsString(type, props));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

// Polyfills for test environment
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;

let React;
let ReactFlightDOMServer;

describe('ReactFlightDOM', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactFlightDOMServer = require('react-dom/unstable-flight-server.browser');
});

async function readResult(stream) {
let reader = stream.getReader();
let result = '';
while (true) {
let {done, value} = await reader.read();
if (done) {
return result;
}
result += Buffer.from(value).toString('utf8');
}
}

it('should resolve HTML', async () => {
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}

let model = {
html: <HTML />,
};
let stream = ReactFlightDOMServer.renderToReadableStream(model);
jest.runAllTimers();
let result = JSON.parse(await readResult(stream));
expect(result).toEqual({
html: '<div><span>hello</span><span>world</span></div>',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/

'use strict';

let Stream;
let React;
let ReactFlightDOMServer;

describe('ReactFlightDOM', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactFlightDOMServer = require('react-dom/unstable-flight-server');
Stream = require('stream');
});

function getTestWritable() {
let writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.result = '';
writable.on('data', chunk => (writable.result += chunk));
return writable;
}

it('should resolve HTML', () => {
function Text({children}) {
return <span>{children}</span>;
}
function HTML() {
return (
<div>
<Text>hello</Text>
<Text>world</Text>
</div>
);
}

let writable = getTestWritable();
let model = {
html: <HTML />,
};
ReactFlightDOMServer.pipeToNodeWritable(model, writable);
jest.runAllTimers();
let result = JSON.parse(writable.result);
expect(result).toEqual({
html: '<div><span>hello</span><span>world</span></div>',
});
});
});
2 changes: 1 addition & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
createRequest,
startWork,
startFlowing,
} from 'react-stream/inline.dom-browser';
} from 'react-server/inline.dom-browser';

function renderToReadableStream(children: ReactNodeList): ReadableStream {
let request;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type {ReactNodeList} from 'shared/ReactTypes';
import type {Writable} from 'stream';

import {createRequest, startWork, startFlowing} from 'react-stream/inline.dom';
import {createRequest, startWork, startFlowing} from 'react-server/inline.dom';

function createDrainHandler(destination, request) {
return () => startFlowing(request, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
* @flow
*/

import {convertStringToBuffer} from 'react-stream/src/ReactFizzHostConfig';
import {convertStringToBuffer} from 'react-server/src/ReactServerHostConfig';

export function formatChunk(type: string, props: Object): Uint8Array {
export function formatChunkAsString(type: string, props: Object): string {
let str = '<' + type + '>';
if (typeof props.children === 'string') {
str += props.children;
}
str += '</' + type + '>';
return convertStringToBuffer(str);
return str;
}

export function formatChunk(type: string, props: Object): Uint8Array {
return convertStringToBuffer(formatChunkAsString(type, props));
}
Loading