Skip to content

Commit

Permalink
Fizz implementation
Browse files Browse the repository at this point in the history
This is semantically the same as just throwing an error. It just triggers
client rendering. The difference is in how it gets logged. On the server
we log to onPostpone instead of onError. On the client this doesn't trigger
a recoverable error. It's just silent since it was intentional.
  • Loading branch information
sebmarkbage committed Aug 17, 2023
1 parent eec516e commit dbcc12f
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 14 deletions.
91 changes: 91 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6040,4 +6040,95 @@ describe('ReactDOMFizzServer', () => {
console.error = originalConsoleError;
}
});

// @gate enablePostpone
it('client renders postponed boundaries without erroring', async () => {
function Postponed({isClient}) {
if (!isClient) {
React.unstable_postpone('testing postpone');
}
return 'client only';
}

function App({isClient}) {
return (
<div>
<Suspense fallback={'loading...'}>
<Postponed isClient={isClient} />
</Suspense>
</div>
);
}

const errors = [];

await act(() => {
const {pipe} = renderToPipeableStream(<App isClient={false} />, {
onError(error) {
errors.push(error.message);
},
});
pipe(writable);
});

expect(getVisibleChildren(container)).toEqual(<div>loading...</div>);

ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
onRecoverableError(error) {
errors.push(error.message);
},
});
await waitForAll([]);
// Postponing should not be logged as a recoverable error since it's intentional.
expect(errors).toEqual([]);
expect(getVisibleChildren(container)).toEqual(<div>client only</div>);
});

// @gate enablePostpone
it('errors if trying to postpone outside a Suspense boundary', async () => {
function Postponed() {
React.unstable_postpone('testing postpone');
return 'client only';
}

function App() {
return (
<div>
<Postponed />
</div>
);
}

const errors = [];
const fatalErrors = [];
const postponed = [];
let written = false;

const testWritable = new Stream.Writable();
testWritable._write = (chunk, encoding, next) => {
written = true;
};

await act(() => {
const {pipe} = renderToPipeableStream(<App />, {
onPostpone(reason) {
postponed.push(reason);
},
onError(error) {
errors.push(error.message);
},
onShellError(error) {
fatalErrors.push(error.message);
},
});
pipe(testWritable);
});

expect(written).toBe(false);
// Postponing is not logged as an error but as a postponed reason.
expect(errors).toEqual([]);
expect(postponed).toEqual(['testing postpone']);
// However, it does error the shell.
expect(fatalErrors).toEqual(['testing postpone']);
});
});
39 changes: 39 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,43 @@ describe('ReactDOMFizzServerBrowser', () => {
`"<link rel="preload" href="init.js" as="script" fetchPriority="low" nonce="R4nd0m"/><link rel="modulepreload" href="init.mjs" fetchPriority="low" nonce="R4nd0m"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
);
});

// @gate enablePostpone
it('errors if trying to postpone outside a Suspense boundary', async () => {
function Postponed() {
React.unstable_postpone('testing postpone');
return 'client only';
}

function App() {
return (
<div>
<Postponed />
</div>
);
}

const errors = [];
const postponed = [];

let caughtError = null;
try {
await ReactDOMFizzServer.renderToReadableStream(<App />, {
onError(error) {
errors.push(error.message);
},
onPostpone(reason) {
postponed.push(reason);
},
});
} catch (error) {
caughtError = error;
}

// Postponing is not logged as an error but as a postponed reason.
expect(errors).toEqual([]);
expect(postponed).toEqual(['testing postpone']);
// However, it does error the shell.
expect(caughtError.message).toEqual('testing postpone');
});
});
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Options = {
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

Expand Down Expand Up @@ -100,6 +101,7 @@ function renderToReadableStream(
onShellReady,
onShellError,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Options = {
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

Expand Down Expand Up @@ -101,6 +102,7 @@ function renderToReadableStream(
onShellReady,
onShellError,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Options = {
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

Expand Down Expand Up @@ -100,6 +101,7 @@ function renderToReadableStream(
onShellReady,
onShellError,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Options = {
onShellError?: (error: mixed) => void,
onAllReady?: () => void,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

Expand Down Expand Up @@ -80,6 +81,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
options ? options.onShellReady : undefined,
options ? options.onShellError : undefined,
undefined,
options ? options.onPostpone : undefined,
);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Options = {
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

Expand Down Expand Up @@ -85,6 +86,7 @@ function prerender(
undefined,
undefined,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Options = {
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

Expand Down Expand Up @@ -85,6 +86,7 @@ function prerender(
undefined,
undefined,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Options = {
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};

Expand Down Expand Up @@ -99,6 +100,7 @@ function prerenderToNodeStreams(
undefined,
undefined,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
Expand Down
1 change: 1 addition & 0 deletions packages/react-dom/src/server/ReactDOMLegacyServerImpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function renderToStringImpl(
onShellReady,
undefined,
undefined,
undefined,
);
startWork(request);
// If anything suspended and is still pending, we'll abort it before writing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function renderToNodeStreamImpl(
onAllReady,
undefined,
undefined,
undefined,
);
destination.request = request;
startWork(request);
Expand Down
29 changes: 17 additions & 12 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import {
enableHostSingletons,
enableFormActions,
enableAsyncActions,
enablePostpone,
} from 'shared/ReactFeatureFlags';
import isArray from 'shared/isArray';
import shallowEqual from 'shared/shallowEqual';
Expand Down Expand Up @@ -2867,19 +2868,23 @@ function updateDehydratedSuspenseComponent(
({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
}

let error;
if (message) {
// eslint-disable-next-line react-internal/prod-error-codes
error = new Error(message);
} else {
error = new Error(
'The server could not finish this Suspense boundary, likely ' +
'due to an error during server rendering. Switched to ' +
'client rendering.',
);
let capturedValue = null;
// TODO: Figure out a better signal than encoding a magic digest value.
if (!enablePostpone || digest !== 'POSTPONE') {
let error;
if (message) {
// eslint-disable-next-line react-internal/prod-error-codes
error = new Error(message);
} else {
error = new Error(
'The server could not finish this Suspense boundary, likely ' +
'due to an error during server rendering. Switched to ' +
'client rendering.',
);
}
(error: any).digest = digest;
capturedValue = createCapturedValue<mixed>(error, digest, stack);
}
(error: any).digest = digest;
const capturedValue = createCapturedValue<mixed>(error, digest, stack);
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
Expand Down
Loading

0 comments on commit dbcc12f

Please sign in to comment.