@samitouri / QOS-React / commits / 7d9f876cbc

[Fizz] Detatch boundary after flushing segment with boundary (#34694)

When we flush a Suspense boundary we might not flush the fallback segment, it might only flush a placeholder instead. In this case the segment can flush again but we do not want to flush the boundary itself a second time. We now detach the boundary after flushing it. better solution to: https://github.com/facebook/react/pull/34668

Josh Story committed Oct 2, 2025 at 13:21 UTC 7d9f876cbc7e9363092e60436704cf8ae435b969
2 files changed +58 -2
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+52
@@ -10832,4 +10832,56 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue
10832 </html>,
10833 );
10834 });
10835 +
10836 + it('not error when a suspended fallback segment directly inside another Suspense is abandoned', async () => {
10837 + function SuspendForever() {
10838 + React.use(new Promise(() => {}));
10839 + }
10840 +
10841 + let resolve = () => {};
10842 + const suspendPromise = new Promise(r => {
10843 + resolve = r;
10844 + });
10845 + function Suspend() {
10846 + return React.use(suspendPromise);
10847 + }
10848 +
10849 + function App() {
10850 + return (
10851 + <html>
10852 + <body>
10853 + <Suspense fallback="outer">
10854 + <Suspense fallback={<SuspendForever />}>
10855 + <span>hello world</span>
10856 + <span>
10857 + <Suspend />
10858 + </span>
10859 + </Suspense>
10860 + </Suspense>
10861 + </body>
10862 + </html>
10863 + );
10864 + }
10865 +
10866 + await act(async () => {
10867 + const {pipe} = renderToPipeableStream(<App />, {
10868 + onError() {},
10869 + });
10870 + pipe(writable);
10871 + });
10872 +
10873 + await act(() => {
10874 + resolve('!');
10875 + });
10876 +
10877 + expect(getVisibleChildren(document)).toEqual(
10878 + <html>
10879 + <head />
10880 + <body>
10881 + <span>hello world</span>
10882 + <span>!</span>
10883 + </body>
10884 + </html>,
10885 + );
10886 + });
10887 });
packages/react-server/src/ReactFizzServer.js
+6 -2
@@ -342,7 +342,7 @@ type Segment = {
342 // The context that this segment was created in.
343 parentFormatContext: FormatContext,
344 // If this segment represents a fallback, this is the content that will replace that fallback.
345 - +boundary: null | SuspenseBoundary,
345 + boundary: null | SuspenseBoundary,
346 // used to discern when text separator boundaries are needed
347 lastPushedText: boolean,
348 textEmbedded: boolean,
@@ -5681,6 +5681,10 @@ function flushSegment(
5681 return flushSubtree(request, destination, segment, hoistableState);
5682 }
5683
5684 + // We're going to write the boundary. We don't need to maintain this reference since
5685 + // we might reflush this segment at a later time (if it aborts and we inlined) but
5686 + // we don't want to reflush the boundary
5687 + segment.boundary = null;
5688 boundary.parentFlushed = true;
5689 // This segment is a Suspense boundary. We need to decide whether to
5690 // emit the content or the fallback now.
@@ -5952,7 +5956,7 @@ function flushPartiallyCompletedSegment(
5956 segment: Segment,
5957 ): boolean {
5958 if (segment.status === FLUSHED) {
5955 - // We've already flushed this inline.
5959 + // We've already flushed this inline
5960 return true;
5961 }
5962