Skip to content

Commit e7be19d

Browse files
committed
[Flight] Allow aborting during render
Previously if you aborted during a render the currently rendering task would itself be aborted which will cause the entire model to be replaced by the aborted error rather than just the slot currently being rendered. This change updates the abort logic to mark currently rendering tasks as aborted but allowing the current render to emit a partially serialized model with an error reference in place of the current model. The intent is to support aborting from rendering synchronously, in microtasks (after an await or in a .then) and in lazy initializers. We don't specifically support aborting from things like proxies that might be triggered during serialization of props
1 parent c865358 commit e7be19d

File tree

3 files changed

+429
-21
lines changed

3 files changed

+429
-21
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js

Lines changed: 309 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,38 @@ describe('ReactFlightDOM', () => {
107107
return maybePromise;
108108
}
109109

110+
async function readInto(
111+
container: Document | HTMLElement,
112+
stream: ReadableStream,
113+
) {
114+
const reader = stream.getReader();
115+
const decoder = new TextDecoder();
116+
let content = '';
117+
while (true) {
118+
const {done, value} = await reader.read();
119+
if (done) {
120+
content += decoder.decode();
121+
break;
122+
}
123+
content += decoder.decode(value, {stream: true});
124+
}
125+
if (container.nodeType === 9 /* DOCUMENT */) {
126+
const doc = new JSDOM(content).window.document;
127+
container.documentElement.innerHTML = doc.documentElement.innerHTML;
128+
while (container.documentElement.attributes.length > 0) {
129+
container.documentElement.removeAttribute(
130+
container.documentElement.attributes[0].name,
131+
);
132+
}
133+
const attrs = doc.documentElement.attributes;
134+
for (let i = 0; i < attrs.length; i++) {
135+
container.documentElement.setAttribute(attrs[i].name, attrs[i].value);
136+
}
137+
} else {
138+
container.innerHTML = content;
139+
}
140+
}
141+
110142
function getTestStream() {
111143
const writable = new Stream.PassThrough();
112144
const readable = new ReadableStream({
@@ -1633,20 +1665,8 @@ describe('ReactFlightDOM', () => {
16331665
ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
16341666
});
16351667

1636-
const decoder = new TextDecoder();
1637-
const reader = fizzReadable.getReader();
1638-
let content = '';
1639-
while (true) {
1640-
const {done, value} = await reader.read();
1641-
if (done) {
1642-
content += decoder.decode();
1643-
break;
1644-
}
1645-
content += decoder.decode(value, {stream: true});
1646-
}
1647-
1648-
const doc = new JSDOM(content).window.document;
1649-
expect(getMeaningfulChildren(doc)).toEqual(
1668+
await readInto(document, fizzReadable);
1669+
expect(getMeaningfulChildren(document)).toEqual(
16501670
<html>
16511671
<head>
16521672
<link rel="dns-prefetch" href="d before" />
@@ -1912,4 +1932,279 @@ describe('ReactFlightDOM', () => {
19121932
});
19131933
expect(container.innerHTML).toBe('Hello World');
19141934
});
1935+
1936+
it('can abort synchronously during render', async () => {
1937+
let siblingDidRender = false;
1938+
function Sibling() {
1939+
siblingDidRender = true;
1940+
return <p>sibling</p>;
1941+
}
1942+
1943+
function App() {
1944+
return (
1945+
<Suspense fallback={<p>loading...</p>}>
1946+
<ComponentThatAborts />
1947+
<Sibling />
1948+
<div>
1949+
<Sibling />
1950+
</div>
1951+
</Suspense>
1952+
);
1953+
}
1954+
1955+
const abortRef = {current: null};
1956+
function ComponentThatAborts() {
1957+
abortRef.current();
1958+
return <p>hello world</p>;
1959+
}
1960+
1961+
const {writable: flightWritable, readable: flightReadable} =
1962+
getTestStream();
1963+
1964+
await serverAct(() => {
1965+
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
1966+
<App />,
1967+
webpackMap,
1968+
);
1969+
abortRef.current = abort;
1970+
pipe(flightWritable);
1971+
});
1972+
1973+
expect(siblingDidRender).toBe(false);
1974+
1975+
const response =
1976+
ReactServerDOMClient.createFromReadableStream(flightReadable);
1977+
1978+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1979+
1980+
function ClientApp() {
1981+
return use(response);
1982+
}
1983+
1984+
const shellErrors = [];
1985+
await serverAct(async () => {
1986+
ReactDOMFizzServer.renderToPipeableStream(
1987+
React.createElement(ClientApp),
1988+
{
1989+
onShellError(error) {
1990+
shellErrors.push(error.message);
1991+
},
1992+
},
1993+
).pipe(fizzWritable);
1994+
});
1995+
1996+
expect(shellErrors).toEqual([]);
1997+
1998+
const container = document.createElement('div');
1999+
await readInto(container, fizzReadable);
2000+
expect(getMeaningfulChildren(container)).toEqual(<p>loading...</p>);
2001+
});
2002+
2003+
it('can abort during render in an async tick', async () => {
2004+
let siblingDidRender = false;
2005+
function DidRender({children}) {
2006+
siblingDidRender = true;
2007+
}
2008+
2009+
async function Sibling() {
2010+
return (
2011+
<DidRender>
2012+
<p>sibling</p>
2013+
</DidRender>
2014+
);
2015+
}
2016+
2017+
function App() {
2018+
return (
2019+
<Suspense fallback={<p>loading...</p>}>
2020+
<ComponentThatAborts />
2021+
<Sibling />
2022+
</Suspense>
2023+
);
2024+
}
2025+
2026+
const abortRef = {current: null};
2027+
async function ComponentThatAborts() {
2028+
await 1;
2029+
abortRef.current();
2030+
return <p>hello world</p>;
2031+
}
2032+
2033+
const {writable: flightWritable, readable: flightReadable} =
2034+
getTestStream();
2035+
2036+
await serverAct(() => {
2037+
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2038+
<App />,
2039+
webpackMap,
2040+
);
2041+
abortRef.current = abort;
2042+
pipe(flightWritable);
2043+
});
2044+
2045+
expect(siblingDidRender).toBe(false);
2046+
2047+
const response =
2048+
ReactServerDOMClient.createFromReadableStream(flightReadable);
2049+
2050+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2051+
2052+
function ClientApp() {
2053+
return use(response);
2054+
}
2055+
2056+
const shellErrors = [];
2057+
await serverAct(async () => {
2058+
ReactDOMFizzServer.renderToPipeableStream(
2059+
React.createElement(ClientApp),
2060+
{
2061+
onShellError(error) {
2062+
shellErrors.push(error.message);
2063+
},
2064+
},
2065+
).pipe(fizzWritable);
2066+
});
2067+
2068+
expect(shellErrors).toEqual([]);
2069+
2070+
const container = document.createElement('div');
2071+
await readInto(container, fizzReadable);
2072+
expect(getMeaningfulChildren(container)).toEqual(<p>loading...</p>);
2073+
});
2074+
2075+
it('can abort during render in a lazy initializer for a component', async () => {
2076+
let siblingDidRender = false;
2077+
2078+
function Sibling() {
2079+
siblingDidRender = true;
2080+
return <p>sibling</p>;
2081+
}
2082+
2083+
function App() {
2084+
return (
2085+
<Suspense fallback={<p>loading...</p>}>
2086+
<LazyAbort />
2087+
<Sibling />
2088+
</Suspense>
2089+
);
2090+
}
2091+
2092+
const abortRef = {current: null};
2093+
const LazyAbort = React.lazy(() => {
2094+
abortRef.current();
2095+
return Promise.resolve({
2096+
default: function LazyComponent() {
2097+
return <p>hello world</p>;
2098+
},
2099+
});
2100+
});
2101+
2102+
const {writable: flightWritable, readable: flightReadable} =
2103+
getTestStream();
2104+
2105+
await serverAct(() => {
2106+
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2107+
<App />,
2108+
webpackMap,
2109+
);
2110+
abortRef.current = abort;
2111+
pipe(flightWritable);
2112+
});
2113+
2114+
expect(siblingDidRender).toBe(false);
2115+
2116+
const response =
2117+
ReactServerDOMClient.createFromReadableStream(flightReadable);
2118+
2119+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2120+
2121+
function ClientApp() {
2122+
return use(response);
2123+
}
2124+
2125+
const shellErrors = [];
2126+
await serverAct(async () => {
2127+
ReactDOMFizzServer.renderToPipeableStream(
2128+
React.createElement(ClientApp),
2129+
{
2130+
onShellError(error) {
2131+
shellErrors.push(error.message);
2132+
},
2133+
},
2134+
).pipe(fizzWritable);
2135+
});
2136+
2137+
expect(shellErrors).toEqual([]);
2138+
2139+
const container = document.createElement('div');
2140+
await readInto(container, fizzReadable);
2141+
expect(getMeaningfulChildren(container)).toEqual(<p>loading...</p>);
2142+
});
2143+
2144+
it('can abort during render in a lazy initializer for an element', async () => {
2145+
let siblingDidRender = false;
2146+
2147+
function Sibling() {
2148+
siblingDidRender = true;
2149+
return <p>sibling</p>;
2150+
}
2151+
2152+
function App() {
2153+
return (
2154+
<Suspense fallback={<p>loading...</p>}>
2155+
{lazyAbort}
2156+
<Sibling />
2157+
</Suspense>
2158+
);
2159+
}
2160+
2161+
const abortRef = {current: null};
2162+
const lazyAbort = React.lazy(() => {
2163+
abortRef.current();
2164+
return Promise.resolve({
2165+
default: <p>hello world</p>,
2166+
});
2167+
});
2168+
2169+
const {writable: flightWritable, readable: flightReadable} =
2170+
getTestStream();
2171+
2172+
await serverAct(() => {
2173+
const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2174+
<App />,
2175+
webpackMap,
2176+
);
2177+
abortRef.current = abort;
2178+
pipe(flightWritable);
2179+
});
2180+
2181+
expect(siblingDidRender).toBe(false);
2182+
2183+
const response =
2184+
ReactServerDOMClient.createFromReadableStream(flightReadable);
2185+
2186+
const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2187+
2188+
function ClientApp() {
2189+
return use(response);
2190+
}
2191+
2192+
const shellErrors = [];
2193+
await serverAct(async () => {
2194+
ReactDOMFizzServer.renderToPipeableStream(
2195+
React.createElement(ClientApp),
2196+
{
2197+
onShellError(error) {
2198+
shellErrors.push(error.message);
2199+
},
2200+
},
2201+
).pipe(fizzWritable);
2202+
});
2203+
2204+
expect(shellErrors).toEqual([]);
2205+
2206+
const container = document.createElement('div');
2207+
await readInto(container, fizzReadable);
2208+
expect(getMeaningfulChildren(container)).toEqual(<p>loading...</p>);
2209+
});
19152210
});

0 commit comments

Comments
 (0)