Skip to content
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

[Fizz] Expose a method to abort a pending request #21027

Merged
merged 5 commits into from
Mar 18, 2021
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
5 changes: 4 additions & 1 deletion fixtures/fizz-ssr-browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ <h1>Fizz Example</h1>
<script src="../../build/node_modules/react-dom/umd/react-dom-unstable-fizz.browser.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<script type="text/babel">
let stream = ReactDOMFizzServer.renderToReadableStream(<body>Success</body>);
let controller = new AbortController();
let stream = ReactDOMFizzServer.renderToReadableStream(<body>Success</body>, {
signal: controller.signal,
});
let response = new Response(stream, {
headers: {'Content-Type': 'text/html'},
});
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@babel/preset-react": "^7.10.4",
"@babel/traverse": "^7.11.0",
"@mattiasbuelens/web-streams-polyfill": "^0.3.2",
"abort-controller": "^3.0.0",
"art": "0.10.1",
"babel-eslint": "^10.0.3",
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
Expand Down
51 changes: 51 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,55 @@ describe('ReactDOMFizzServer', () => {
// Now it's hydrated.
expect(ref.current).toBe(h1);
});

// @gate experimental
it('client renders a boundary if it does not resolve before aborting', async () => {
function App() {
return (
<div>
<Suspense fallback="Loading...">
<h1>
<AsyncText text="Hello" />
</h1>
</Suspense>
</div>
);
}

let controls;
await act(async () => {
controls = ReactDOMFizzServer.pipeToNodeWritable(<App />, writable);
});

// We're still showing a fallback.

// Attempt to hydrate the content.
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
Scheduler.unstable_flushAll();

// We're still loading because we're waiting for the server to stream more content.
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);

// We abort the server response.
await act(async () => {
controls.abort();
});

// We still can't render it on the client.
Scheduler.unstable_flushAll();
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);

// We now resolve it on the client.
resolveText('Hello');

Scheduler.unstable_flushAll();

// The client rendered HTML is now in place.
expect(getVisibleChildren(container)).toEqual(
<div>
<h1>Hello</h1>
</div>,
);
});
});
19 changes: 19 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// Polyfills for test environment
global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.AbortController = require('abort-controller');

let React;
let ReactDOMFizzServer;
Expand Down Expand Up @@ -110,4 +111,22 @@ describe('ReactDOMFizzServer', () => {
const result = await readResult(stream);
expect(result).toContain('Loading');
});

// @gate experimental
it('should be able to complete by aborting even if the promise never resolves', async () => {
const controller = new AbortController();
const stream = ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
{signal: controller.signal},
);

controller.abort();

const result = await readResult(stream);
expect(result).toContain('Loading');
});
});
50 changes: 50 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,54 @@ describe('ReactDOMFizzServer', () => {
expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
});

// @gate experimental
it('should not attempt to render the fallback if the main content completes first', async () => {
const {writable, output, completed} = getTestWritable();

let renderedFallback = false;
function Fallback() {
renderedFallback = true;
return 'Loading...';
}
function Content() {
return 'Hi';
}
ReactDOMFizzServer.pipeToNodeWritable(
<Suspense fallback={<Fallback />}>
<Content />
</Suspense>,
writable,
);

await completed;

expect(output.result).toContain('Hi');
expect(output.result).not.toContain('Loading');
expect(renderedFallback).toBe(false);
});

// @gate experimental
it('should be able to complete by aborting even if the promise never resolves', async () => {
const {writable, output, completed} = getTestWritable();
const {abort} = ReactDOMFizzServer.pipeToNodeWritable(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
writable,
);

jest.runAllTimers();

expect(output.result).toContain('Loading');

abort();

await completed;

expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
});
});
18 changes: 17 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,26 @@ import {
createRequest,
startWork,
startFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

function renderToReadableStream(children: ReactNodeList): ReadableStream {
type Options = {
signal?: AbortSignal,
};

function renderToReadableStream(
children: ReactNodeList,
options?: Options,
): ReadableStream {
let request;
if (options && options.signal) {
const signal = options.signal;
const listener = () => {
abort(request);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
return new ReadableStream({
start(controller) {
request = createRequest(children, controller);
Expand Down
14 changes: 13 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,31 @@ import {
createRequest,
startWork,
startFlowing,
abort,
} from 'react-server/src/ReactFizzServer';

function createDrainHandler(destination, request) {
return () => startFlowing(request);
}

type Controls = {
// Cancel any pending I/O and put anything remaining into
// client rendered mode.
abort(): void,
};

function pipeToNodeWritable(
children: ReactNodeList,
destination: Writable,
): void {
): Controls {
const request = createRequest(children, destination);
destination.on('drain', createDrainHandler(destination, request));
startWork(request);
return {
abort() {
abort(request);
},
};
}

export {pipeToNodeWritable};
3 changes: 3 additions & 0 deletions packages/react-noop-renderer/src/ReactNoopServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ function render(children: React$Element<any>): Destination {
placeholders: new Map(),
segments: new Map(),
stack: [],
abort() {
ReactNoopServer.abort(request);
},
};
const request = ReactNoopServer.createRequest(children, destination);
ReactNoopServer.startWork(request);
Expand Down
Loading