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

Add onRecoverableError option to hydrateRoot, createRoot #23207

Merged
merged 8 commits into from Feb 4, 2022

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Jan 28, 2022

When an error is thrown during hydration, we fallback to client rendering, without triggering an error boundary. This is good because, in many cases, the UI will recover and the user won't even notice that something has gone wrong behind the scenes.

However, we shouldn't recover from these errors silently, because the underlying cause might be pretty serious. Server-client mismatches are not supposed to happen, even if UI doesn't break from the users perspective. Ignoring them could lead to worse problems later. De-opting from server to client rendering could also be a significant performance regression, depending on the scope of the UI it affects.

So we need a way to log when hydration errors occur.

This adds a new option for hydrateRoot called onRecoverableError. It's symmetrical to the server renderer's onError option, and serves the same purpose.

When no option is provided, the default behavior is to schedule a browser task and rethrow the error. This will trigger the normal browser behavior for errors, including dispatching an error event. If the app already has error monitoring, this likely will just work as expected without additional configuration.

However, we can also expose additional metadata about these errors, like which Suspense boundaries were affected by the de-opt to client rendering. (I have not exposed any metadata in this PR, but we intend to add it in the future.)

In addition to errors that occur during hydration, we also log other types of recoverable errors. For example, some errors that are a result of a concurrent data race can be recovered by de-opting to synchronous rendering; blocking the main thread fixes them by prevents subsequent races. The logic for de-opting to synchronous rendering already existed. The only thing that has changed is that we now log the errors instead of silently proceeding.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jan 28, 2022
@acdlite acdlite force-pushed the onhydrationerror branch 2 times, most recently from 6af98f4 to 17f66a9 Compare January 28, 2022 18:45
@sizebot
Copy link

sizebot commented Jan 28, 2022

Comparing: 5318971...24abe60

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js +0.45% 129.75 kB 130.34 kB +0.53% 41.59 kB 41.81 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.43% 134.93 kB 135.52 kB +0.51% 43.12 kB 43.34 kB
facebook-www/ReactDOM-prod.classic.js +0.58% 428.75 kB 431.25 kB +0.51% 78.70 kB 79.10 kB
facebook-www/ReactDOM-prod.modern.js +0.59% 418.74 kB 421.19 kB +0.55% 77.25 kB 77.67 kB
facebook-www/ReactDOMForked-prod.classic.js +0.58% 428.75 kB 431.25 kB +0.51% 78.70 kB 79.10 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js +0.65% 236.31 kB 237.84 kB +0.49% 43.34 kB 43.55 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js +0.61% 251.41 kB 252.94 kB +0.48% 45.73 kB 45.95 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.60% 406.94 kB 409.39 kB +0.56% 76.82 kB 77.24 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.59% 420.32 kB 422.81 kB +0.52% 78.92 kB 79.33 kB
facebook-www/ReactDOM-prod.modern.js +0.59% 418.74 kB 421.19 kB +0.55% 77.25 kB 77.67 kB
facebook-www/ReactDOMForked-prod.modern.js +0.59% 418.74 kB 421.19 kB +0.55% 77.25 kB 77.68 kB
facebook-www/ReactDOM-prod.classic.js +0.58% 428.75 kB 431.25 kB +0.51% 78.70 kB 79.10 kB
facebook-www/ReactDOMForked-prod.classic.js +0.58% 428.75 kB 431.25 kB +0.51% 78.70 kB 79.10 kB
facebook-www/ReactART-prod.modern.js +0.55% 268.75 kB 270.23 kB +0.54% 47.83 kB 48.09 kB
react-native/implementations/ReactFabric-prod.js +0.54% 272.56 kB 274.04 kB +0.43% 49.01 kB 49.22 kB
facebook-www/ReactART-prod.classic.js +0.54% 276.44 kB 277.92 kB +0.47% 49.13 kB 49.36 kB
facebook-www/ReactDOM-profiling.modern.js +0.53% 449.37 kB 451.76 kB +0.46% 81.79 kB 82.17 kB
facebook-www/ReactDOMForked-profiling.modern.js +0.53% 449.37 kB 451.76 kB +0.46% 81.80 kB 82.18 kB
facebook-www/ReactDOM-profiling.classic.js +0.53% 459.45 kB 461.88 kB +0.48% 83.30 kB 83.70 kB
facebook-www/ReactDOMForked-profiling.classic.js +0.53% 459.45 kB 461.88 kB +0.48% 83.30 kB 83.70 kB
react-native/implementations/ReactNativeRenderer-prod.js +0.53% 281.19 kB 282.67 kB +0.43% 50.65 kB 50.87 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js +0.53% 726.18 kB 729.99 kB +0.66% 154.45 kB 155.47 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js +0.53% 726.18 kB 729.99 kB +0.66% 154.45 kB 155.47 kB
react-native/implementations/ReactFabric-prod.fb.js +0.52% 284.04 kB 285.52 kB +0.39% 51.23 kB 51.43 kB
react-native/implementations/ReactNativeRenderer-prod.fb.js +0.51% 288.00 kB 289.48 kB +0.41% 52.05 kB 52.27 kB
react-native/implementations/ReactFabric-profiling.js +0.51% 291.57 kB 293.05 kB +0.43% 52.16 kB 52.38 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +0.51% 753.08 kB 756.90 kB +0.65% 160.11 kB 161.15 kB
react-native/implementations/ReactNativeRenderer-profiling.js +0.49% 300.24 kB 301.72 kB +0.41% 53.79 kB 54.01 kB
react-native/implementations/ReactFabric-profiling.fb.js +0.48% 311.06 kB 312.55 kB +0.45% 55.43 kB 55.68 kB
react-native/implementations/ReactNativeRenderer-profiling.fb.js +0.47% 314.91 kB 316.40 kB +0.44% 56.26 kB 56.51 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.47% 1,008.67 kB 1,013.38 kB +0.54% 227.28 kB 228.50 kB
oss-stable-semver/react-dom/cjs/react-dom.development.js +0.46% 997.31 kB 1,001.93 kB +0.55% 223.86 kB 225.09 kB
oss-stable/react-dom/cjs/react-dom.development.js +0.46% 997.31 kB 1,001.93 kB +0.55% 223.86 kB 225.09 kB
oss-stable-semver/react-dom/umd/react-dom.development.js +0.46% 1,046.85 kB 1,051.64 kB +0.54% 226.33 kB 227.54 kB
oss-stable/react-dom/umd/react-dom.development.js +0.46% 1,046.85 kB 1,051.64 kB +0.54% 226.33 kB 227.54 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.46% 1,035.47 kB 1,040.19 kB +0.53% 232.68 kB 233.90 kB
oss-stable-semver/react-dom/cjs/react-dom.production.min.js +0.45% 129.75 kB 130.34 kB +0.53% 41.59 kB 41.81 kB
oss-stable/react-dom/cjs/react-dom.production.min.js +0.45% 129.75 kB 130.34 kB +0.53% 41.59 kB 41.81 kB
oss-stable-semver/react-dom/umd/react-dom.production.min.js +0.45% 129.88 kB 130.47 kB +0.50% 42.27 kB 42.48 kB
oss-stable/react-dom/umd/react-dom.production.min.js +0.45% 129.88 kB 130.47 kB +0.50% 42.27 kB 42.48 kB
oss-experimental/react-dom/cjs/react-dom.development.js +0.45% 1,026.66 kB 1,031.27 kB +0.51% 230.02 kB 231.20 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.min.js +0.45% 91.84 kB 92.25 kB +0.64% 28.28 kB 28.46 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.min.js +0.45% 91.84 kB 92.25 kB +0.64% 28.28 kB 28.46 kB
oss-experimental/react-dom/umd/react-dom.development.js +0.44% 1,077.61 kB 1,082.41 kB +0.52% 232.58 kB 233.78 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.43% 134.93 kB 135.52 kB +0.51% 43.12 kB 43.34 kB
oss-experimental/react-dom/umd/react-dom.production.min.js +0.43% 134.98 kB 135.57 kB +0.45% 43.78 kB 43.98 kB
facebook-relay/flight/ReactFlightNativeRelayServer-prod.js +0.43% 15.75 kB 15.82 kB +0.32% 4.09 kB 4.11 kB
facebook-www/ReactFlightDOMRelayServer-prod.classic.js +0.43% 15.77 kB 15.84 kB +0.32% 4.11 kB 4.13 kB
facebook-www/ReactFlightDOMRelayServer-prod.modern.js +0.43% 15.77 kB 15.84 kB +0.32% 4.11 kB 4.13 kB
facebook-www/ReactDOMForked-dev.modern.js +0.43% 1,101.91 kB 1,106.62 kB +0.50% 243.80 kB 245.02 kB
facebook-www/ReactDOM-dev.modern.js +0.43% 1,101.91 kB 1,106.62 kB +0.50% 243.80 kB 245.02 kB
oss-stable-semver/react-dom/cjs/react-dom-testing.production.min.js +0.43% 124.48 kB 125.01 kB +0.51% 39.93 kB 40.13 kB
oss-stable/react-dom/cjs/react-dom-testing.production.min.js +0.43% 124.48 kB 125.01 kB +0.51% 39.93 kB 40.13 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.min.js +0.43% 96.44 kB 96.85 kB +0.51% 29.55 kB 29.70 kB
oss-stable-semver/react-dom/umd/react-dom.profiling.min.js +0.42% 138.64 kB 139.22 kB +0.48% 44.57 kB 44.78 kB
oss-stable/react-dom/umd/react-dom.profiling.min.js +0.42% 138.64 kB 139.22 kB +0.48% 44.57 kB 44.78 kB
oss-stable-semver/react-dom/cjs/react-dom.profiling.min.js +0.42% 139.19 kB 139.78 kB +0.53% 44.03 kB 44.26 kB
oss-stable/react-dom/cjs/react-dom.profiling.min.js +0.42% 139.19 kB 139.78 kB +0.53% 44.03 kB 44.26 kB
facebook-www/ReactDOMForked-dev.classic.js +0.42% 1,124.07 kB 1,128.80 kB +0.50% 247.94 kB 249.18 kB
facebook-www/ReactDOM-dev.classic.js +0.42% 1,124.07 kB 1,128.80 kB +0.50% 247.94 kB 249.18 kB
oss-stable-semver/react-dom/cjs/react-dom-testing.development.js +0.42% 941.82 kB 945.73 kB +0.48% 213.57 kB 214.59 kB
oss-stable/react-dom/cjs/react-dom-testing.development.js +0.42% 941.82 kB 945.73 kB +0.48% 213.57 kB 214.59 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js +0.41% 624.38 kB 626.97 kB +0.44% 135.51 kB 136.12 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js +0.41% 611.89 kB 614.41 kB +0.46% 134.32 kB 134.94 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js +0.41% 611.89 kB 614.41 kB +0.46% 134.32 kB 134.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.min.js +0.41% 100.74 kB 101.15 kB +0.62% 30.48 kB 30.67 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.min.js +0.41% 100.74 kB 101.15 kB +0.62% 30.48 kB 30.67 kB
oss-experimental/react-dom/umd/react-dom.profiling.min.js +0.41% 143.72 kB 144.31 kB +0.55% 46.05 kB 46.30 kB
oss-experimental/react-dom/cjs/react-dom.profiling.min.js +0.41% 144.37 kB 144.96 kB +0.51% 45.64 kB 45.87 kB
oss-stable-semver/react-test-renderer/umd/react-test-renderer.development.js +0.41% 641.53 kB 644.13 kB +0.45% 135.82 kB 136.43 kB
oss-stable/react-test-renderer/umd/react-test-renderer.development.js +0.41% 641.53 kB 644.13 kB +0.45% 135.82 kB 136.43 kB
oss-experimental/react-dom/cjs/react-dom-testing.development.js +0.40% 987.09 kB 991.00 kB +0.43% 223.68 kB 224.65 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer.production.min.js +0.39% 13.70 kB 13.76 kB +0.51% 3.95 kB 3.97 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer.production.min.js +0.39% 13.70 kB 13.76 kB +0.51% 3.95 kB 3.97 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer.production.min.js +0.39% 13.70 kB 13.76 kB +0.51% 3.95 kB 3.97 kB
oss-experimental/react-dom/cjs/react-dom-testing.production.min.js +0.39% 135.27 kB 135.80 kB +0.44% 43.66 kB 43.85 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js +0.39% 638.57 kB 641.09 kB +0.43% 140.02 kB 140.62 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-persistent.production.min.js +0.39% 13.77 kB 13.82 kB +0.50% 3.97 kB 3.99 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-persistent.production.min.js +0.39% 13.77 kB 13.82 kB +0.50% 3.97 kB 3.99 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-persistent.production.min.js +0.39% 13.77 kB 13.82 kB +0.50% 3.97 kB 3.99 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.min.js +0.39% 105.34 kB 105.75 kB +0.51% 31.81 kB 31.97 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.development.js +0.39% 669.53 kB 672.13 kB +0.41% 141.53 kB 142.11 kB
oss-stable-semver/react-art/cjs/react-art.development.js +0.39% 667.08 kB 669.66 kB +0.43% 144.16 kB 144.78 kB
oss-stable/react-art/cjs/react-art.development.js +0.39% 667.08 kB 669.66 kB +0.43% 144.16 kB 144.78 kB
facebook-www/ReactTestRenderer-dev.classic.js +0.38% 672.53 kB 675.12 kB +0.41% 145.14 kB 145.73 kB
facebook-www/ReactTestRenderer-dev.modern.js +0.38% 672.54 kB 675.13 kB +0.41% 145.14 kB 145.74 kB
oss-experimental/react-art/cjs/react-art.development.js +0.37% 693.79 kB 696.37 kB +0.40% 149.83 kB 150.42 kB
react-native/implementations/ReactFabric-dev.js +0.36% 719.74 kB 722.33 kB +0.39% 156.53 kB 157.13 kB
facebook-www/ReactART-dev.modern.js +0.35% 747.01 kB 749.66 kB +0.40% 159.47 kB 160.10 kB
react-native/implementations/ReactNativeRenderer-dev.js +0.35% 733.74 kB 736.33 kB +0.40% 159.83 kB 160.47 kB
facebook-www/ReactART-dev.classic.js +0.35% 757.23 kB 759.88 kB +0.40% 161.57 kB 162.22 kB
oss-stable-semver/react-art/umd/react-art.development.js +0.35% 770.18 kB 772.85 kB +0.40% 162.42 kB 163.07 kB
oss-stable/react-art/umd/react-art.development.js +0.35% 770.18 kB 772.85 kB +0.40% 162.42 kB 163.07 kB
react-native/implementations/ReactFabric-dev.fb.js +0.34% 762.00 kB 764.59 kB +0.36% 164.29 kB 164.88 kB
react-native/implementations/ReactNativeRenderer-dev.fb.js +0.33% 774.14 kB 776.73 kB +0.35% 167.22 kB 167.80 kB
oss-experimental/react-art/umd/react-art.development.js +0.33% 798.19 kB 800.86 kB +0.38% 168.04 kB 168.68 kB
oss-stable-semver/react-art/cjs/react-art.production.min.js +0.33% 81.75 kB 82.02 kB +0.40% 25.38 kB 25.49 kB
oss-stable/react-art/cjs/react-art.production.min.js +0.33% 81.75 kB 82.02 kB +0.40% 25.38 kB 25.49 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.min.js +0.32% 84.15 kB 84.43 kB +0.45% 25.96 kB 26.08 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.min.js +0.32% 84.15 kB 84.43 kB +0.45% 25.96 kB 26.08 kB
oss-stable-semver/react-test-renderer/umd/react-test-renderer.production.min.js +0.32% 84.39 kB 84.66 kB +0.46% 26.29 kB 26.41 kB
oss-stable/react-test-renderer/umd/react-test-renderer.production.min.js +0.32% 84.39 kB 84.66 kB +0.46% 26.29 kB 26.41 kB
oss-experimental/react-art/cjs/react-art.production.min.js +0.31% 86.31 kB 86.58 kB +0.42% 26.66 kB 26.77 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.min.js +0.31% 88.69 kB 88.96 kB +0.41% 27.31 kB 27.42 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.production.min.js +0.31% 88.91 kB 89.18 kB +0.40% 27.61 kB 27.72 kB
oss-stable-semver/react-art/umd/react-art.production.min.js +0.23% 117.69 kB 117.96 kB +0.47% 36.51 kB 36.68 kB
oss-stable/react-art/umd/react-art.production.min.js +0.23% 117.69 kB 117.96 kB +0.47% 36.51 kB 36.68 kB
oss-experimental/react-art/umd/react-art.production.min.js +0.22% 122.18 kB 122.45 kB +0.19% 37.89 kB 37.96 kB

Generated by 🚫 dangerJS against 24abe60

@acdlite acdlite force-pushed the onhydrationerror branch 3 times, most recently from fe8c95f to ba4e106 Compare January 31, 2022 19:33
@acdlite acdlite changed the title [RFC] Add onHydrationError option to hydrateRoot Add onRecoverableError option to hydrateRoot, createRoot Jan 31, 2022
@acdlite acdlite marked this pull request as ready for review January 31, 2022 19:50
error: mixed,
): void {
const onRecoverableError = config;
if (onRecoverableError !== null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This feels like a reconciler specific concern to me. Especially since this is likely to grow with other types of callbacks that will have more advanced semantics than this callback will handle.

One case I'd expect we might want is to scope this to subtree. Like error boundaries but for recoverable errors.

The reconciler can make this choice and schedule an IdlePriority callback itself if it wants to. Although it feels a bit presumptuous to do that for the user since you can schedule it in the callback. Although paternalism is on brand for us I guess.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I went back and forth on that. The main reason I ended up adding a host config is because it needs a default fallback behavior; didn't feel right to assume that throwing in a Scheduler task is necessarily the correct default for each environment (although I suppose that's what we do for uncaught errors). An alternative is to resolve the default option within the renderer before passing it to the root constructor but that feels like a superficial difference. Don't mind either way.

Related comment:

// TODO: We have several of these arguments that are conceptually part of the
// host config, but because they are passed in at runtime, we have to thread
// them through the root constructor. Perhaps we should put them all into a
// single type, like a DynamicHostConfig that is defined by the renderer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea I don't think it's always correct to throw in a task so having host config makes sense for the default fallback behavior. It's the main API behavior that doesn't seem right to determine in the host config. In other words, resolving the default option within the renderer would make sense. That also avoids leaking the type to all the host configs.

// Default behavior is to rethrow the error in a separate task. This will
// trigger a browser error event.
scheduleCallback(IdlePriority, () => {
throw error;
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a handleErrorInNextTick function in this file that has different semantics but I guess is similarly a fallback default behavior? Should they be unified?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looks like that method is only used by the queueMicrotask fallback... I'm unsure why that's necessary, to be honest. Feels wrong.

@@ -374,6 +379,25 @@ export function getCurrentEventPriority(): * {
return getEventPriority(currentEvent.type);
}

export function logRecoverableError(
Copy link
Collaborator

Choose a reason for hiding this comment

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

For my own memory later, the reason this wasn't already something that existed for throwing top level is because other top level errors are handled by throwing at the root of the work loop and letting whatever is above handle it.

config: ErrorLoggingConfig,
error: mixed,
): void {
// noop
Copy link
Collaborator

Choose a reason for hiding this comment

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

console.error or something maybe to start with?

// probably want to log any error that is recovered from without
// triggering an error boundary — or maybe even those, too. Need to
// figure out the right API.
logRecoverableError(root.errorLoggingConfig, value);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we probably don't want to log errors to this if they also end up surfacing a user error afterwards. It seems like kind of a misnomer in the API if it also gets passed errors that end up erroring. Even though you could maybe try to dedupe them this seems tricky to do in user space. The idle callback (if we keep it) also makes this even trickier since the error boundaries are not idle.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree but I can't think of a reliable way to implement it that doesn't fail in some edge case. Once it falls back to client rendering, we can't know for sure if a subsequent error is the same one or a different one. So here I erred on the side of logging too much.

To argue for the other approach, you could say that when that situation does happen, it's ok that the first error gets unintentionally silenced because it will be unsilenced after the second error is fixed.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

It could be a follow up but another thing missing here is the scenario where the Suspense boundary was in "client render mode" or ended up being put in client render mode by the server. I.e. a server errored.

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L2652

Similarly, there's still this case where an update forces a hydration but if it can't get the bumped priority or that one failed last time. Then it gets client rendered without an error on either side but that's arguably an error that we can create and pass to the callback.

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L2710

This case in particular suggests that we might want to log this upon commit.

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 31, 2022

It could be a follow up but another thing missing here is the scenario where the Suspense boundary was in "client render mode" or ended up being put in client render mode by the server. I.e. a server errored.

This one I was planning to do in a follow-up. Could also push to a minor release since at least they are already logged on the server.

Similarly, there's still this case where an update forces a hydration but if it can't get the bumped priority or that one failed last time.

Ah yeah forgot about that one. I can add that.

Scheduler.unstable_flushAll();
// Hydration error is logged
expect(Scheduler).toFlushAndYield([
'An error occurred during hydration. The server HTML was replaced ' +
Copy link
Contributor

Choose a reason for hiding this comment

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

This is actually not a real error. What happens is that since <Child> suspended the hydration pointer is off for the remainder where we "attempt to hydrate" (Really we're just "warming up" the components instead of bailing) so everything after a component suspends leads to a hydration mismatch.

@acdlite acdlite force-pushed the onhydrationerror branch 3 times, most recently from 5f37862 to 073ecfd Compare February 3, 2022 01:20
function commitRootImpl(root, renderPriorityLevel) {
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<mixed>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is kind of annoying but there's no obvious place to store these. We don't usually pass things directly from the render phase to the commit phase because we stash it on a fiber.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't get this part. All calls to commitRoot call this with the global workInProgressRootRecoverableErrors which you have access to here too. Other workInProgress globals are accessed here too.

So can't you just avoid passing it and instead access workInProgressRootRecoverableErrors here?

Copy link
Collaborator Author

@acdlite acdlite Feb 4, 2022

Choose a reason for hiding this comment

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

There's an annoying edge case when both 1) you're in a multi-root app 2) the commit phase is delayed with setTimeout (this used to be more common but now it's really just the Suspense train model). During the timeout delay, the next root can start rendering. So you can't access work loop variables directly; you need to stash data somewhere in the fiber tree or on the FiberRoot.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah yes, and with suspense images probably. That is annoying.

This is not the final API but I'm pushing it for discussion purposes.

When an error is thrown during hydration, we fallback to client
rendering, without triggering an error boundary. This is good because,
in many cases, the UI will recover and the user won't even notice that
something has gone wrong behind the scenes.

However, we shouldn't recover from these errors silently, because the
underlying cause might be pretty serious. Server-client mismatches are
not supposed to happen, even if UI doesn't break from the users
perspective. Ignoring them could lead to worse problems later. De-opting
from server to client rendering could also be a significant performance
regression, depending on the scope of the UI it affects.

So we need a way to log when hydration errors occur.

This adds a new option for `hydrateRoot` called `onHydrationError`. It's
symmetrical to the server renderer's `onError` option, and serves the
same purpose.

When no option is provided, the default behavior is to schedule a
browser task and rethrow the error. This will trigger the normal browser
behavior for errors, including dispatching an error event. If the app
already has error monitoring, this likely will just work as expected
without additional configuration.

However, we can also expose additional metadata about these errors, like
which Suspense boundaries were affected by the de-opt to client
rendering. (I have not exposed any metadata in this commit; API needs
more design work.)

There are other situations besides hydration where we recover from an
error without surfacing it to the user, or notifying an error boundary.
For example, if an error occurs during a concurrent render, it could be
due to a data race, so we try again synchronously in case that fixes it.
We should probably expose a way to log these types of errors, too. (Also
not implemented in this commit.)
This expands the scope of onHydrationError to include all errors that
are not surfaced to the UI (an error boundary). In addition to errors
that occur during hydration, this also includes errors that recoverable
by de-opting to synchronous rendering. Typically (or really, by
definition) these errors are the result of a concurrent data race;
blocking the main thread fixes them by prevents subsequent races.

The logic for de-opting to synchronous rendering already existed. The
only thing that has changed is that we now log the errors instead of
silently proceeding.

The logging API has been renamed from onHydrationError
to onRecoverableError.
If the render is interrupted and restarts, we don't want to log the
errors multiple times.

This change only affects errors that are recovered by de-opting to
synchronous rendering; we'll have to do something else for errors
during hydration, since they use a different recovery path.
Similar to previous step.

When an error occurs during hydration, we only want to log it if falling
back to client rendering _succeeds_. If client rendering fails,
the error will get reported to the nearest error boundary, so there's
no need for a duplicate log.

To implement this, I added a list of errors to the hydration context.
If the Suspense boundary successfully completes, they are added to
the main recoverable errors queue (the one I added in the
previous step.)
If onRecoverableError is not provided, we default to rethrowing the
error in a separate task. Originally, I scheduled the task with
idle priority, but @sebmarkbage made the good point that if there are
multiple errors logs, we want to preserve the original order. So I've
switched it to a microtask. The priority can be lowered in userspace
by scheduling an additional task inside onRecoverableError.
Redefines the contract of the host config's logRecoverableError method
to be a default implementation for onRecoverableError if a user-provided
one is not provided when the root is created.
@@ -2091,6 +2106,15 @@ function commitRootImpl(root, renderPriorityLevel) {
// additional work on this root is scheduled.
ensureRootIsScheduled(root, now());

if (recoverableErrors !== null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The timing of these are interesting given that we were talking about logging order. If it's uncaught it'll be logged after this, but if it's caught the logging will happen in an error boundary who is supposed to log it in componentDidCatch which is a layout effect.

So it seems to me that this needs to be logged before the layout effect phase.

Copy link
Collaborator Author

@acdlite acdlite Feb 4, 2022

Choose a reason for hiding this comment

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

That's a good point, though I don't know if we can be sure of the order unless we start tracking the recoverable errors per subtree. If there were recoverable errors and regular errors (componentDidCatch) in the same commit, the current implementation can't tell which happened first because the recoverable error array is shared across the whole tree.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea but if they're separate subtrees they're more likely independent and we could've rendered them in any order.

The cases I'm mostly thinking about is when you have something like a hydration that succeeded and then errored because it wasn't quite fixed. However, I can't think of a case where that wouldn't be two separate consecutive commits.

That would be more of an issue with the throw vs next task throw thing, but not this case.

@@ -601,10 +607,28 @@ function resetHydrationState(): void {
isHydrating = false;
}

export function queueRecoverableHydrationErrors(): void {
Copy link
Collaborator

Choose a reason for hiding this comment

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

These two methods sound the same. Maybe "apply" or "upstream"...?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

upgradeHydrationErrorsToRecoverable?

// Default behavior is to rethrow the error in a separate task. This will
// trigger a browser error event.
queueMicrotask(() => {
throw error;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any worries about the Safari bug that can execute this on unclean stack if iframe is added?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sigh. Yeah.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't really know what the workaround is, though, other than to avoid queueMicrotask. I'm tempted to leave it as-is and leave it to Safari to fix their shit.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe a user space one. It doesn't catch on break etc. but gives you something in the DEV console and in prod.

console.error(...);
window.dispatchEvent(new ErrorEvent(...));

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is one of those options better than the other?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems to me that queueMicrotask doesn't actually solve the problem, does it? Because the rethrown error will be logged before the microtask. So in the default case, the order is still hard error followed by recovered error. So there's no difference from postTask.

Where as the user space log + dispatch would be recovered error followed by hard error.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If you have a recovered error and then a useLayoutEffect or anything causing a second commit in the same task, that then errors because the recovering didn't quite work. Then recovered errors should ideally come first.

Copy link
Collaborator Author

@acdlite acdlite Feb 4, 2022

Choose a reason for hiding this comment

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

Ah when we were discussing error ordering I was thinking about other errors that happen in subsequent events, not the hard error that we throw at the top when there's no error boundary. (I don't think about those usually because I assume most apps have a top-level error boundary.) That makes sense.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

I think everything else is nits or possible follow ups.

@acdlite
Copy link
Collaborator Author

acdlite commented Feb 4, 2022

I'm going to collect the follow ups here so I don't forget:

Will do these before merging:

  • For default logging behavior, switch to reportError, with fallback to console.error in test environments
  • Naming nits

Will do these as follow ups:

  • Log errors before the layout phase. (Maybe? Unclear if it matters unless we also start tracking them per subtree. But we probably should regardless so it's conceptually closer to the ideal.)
  • Log an error in all cases where we fall back to client render, including
    • Boundary suspends during a synchronous render
    • Sync update forces a client render and selective hydration mechanism fails because we don't yield (this is a bug in React but we should still log when it happens)

@acdlite acdlite force-pushed the onhydrationerror branch 3 times, most recently from 537d1ca to 2d8ef49 Compare February 4, 2022 15:36
In modern browsers, reportError will dispatch an error event, emulating
an uncaught JavaScript error. We can do this instead of rethrowing
recoverable errors in a microtask, which is nice because it avoids any
subtle ordering issues.

In older browsers and test environments, we'll fall back
to console.error.
queueRecoverableHydrationErrors -> upgradeHydrationErrorsToRecoverable
@acdlite acdlite merged commit 848e802 into facebook:main Feb 4, 2022
@gaearon
Copy link
Collaborator

gaearon commented Feb 4, 2022

should we add as followups:

facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request Feb 9, 2022
Summary:
This sync includes the following changes:
- **[a3bde7974](facebook/react@a3bde7974 )**: Exclude react-dom/unstable_testing entry point from stable releases ([#23258](facebook/react#23258)) //<Sebastian Markbåge>//
- **[569093276](facebook/react@569093276 )**: Add onErrorShell Callback ([#23247](facebook/react#23247)) //<Sebastian Markbåge>//
- **[0dedfcc68](facebook/react@0dedfcc68 )**: Update the exports field ([#23257](facebook/react#23257)) //<Sebastian Markbåge>//
- **[9d4e8e84f](facebook/react@9d4e8e84f )**: React Native raw event EventEmitter - intended for app-specific perf listeners and debugging ([#23232](facebook/react#23232)) //<Joshua Gross>//
- **[1dece5235](facebook/react@1dece5235 )**: Add back warning with component stack on Hydration mismatch ([#23241](facebook/react#23241)) //<salazarm>//
- **[cd4eb116c](facebook/react@cd4eb116c )**: Revert "update node.js version for CI ([#23236](facebook/react#23236))" ([#23239](facebook/react#23239)) //<dan>//
- **[1d7728bf9](facebook/react@1d7728bf9 )**: update node.js version for CI ([#23236](facebook/react#23236)) //<sunderls>//
- **[848e802d2](facebook/react@848e802d2 )**: Add onRecoverableError option to hydrateRoot, createRoot ([#23207](facebook/react#23207)) //<Andrew Clark>//
- **[5318971f5](facebook/react@5318971f5 )**: Remove logic for multiple error recovery attempts ([#23227](facebook/react#23227)) //<Andrew Clark>//
- **[3a4462129](facebook/react@3a4462129 )**: Disable avoidThisFallback support in Fizz  ([#23224](facebook/react#23224)) //<salazarm>//
- **[0318ac2c4](facebook/react@0318ac2c4 )**: Revert 4f5449 //<Ricky>//
- **[4f5449eb4](facebook/react@4f5449eb4 )**: Remove main from scheduler `index.js` //<Ricky>//
- **[3f5ff16c1](facebook/react@3f5ff16c1 )**: [Hydration] Fallback to client render if server rendered extra nodes ([#23176](facebook/react#23176)) //<salazarm>//
- **[529dc3ce8](facebook/react@529dc3ce8 )**: Fix context providers in SSR when handling multiple requests ([#23171](facebook/react#23171)) //<Fran Dios>//
- **[505c15c9e](facebook/react@505c15c9e )**: Don't inject timeline hooks unless React supports profiling ([#23151](facebook/react#23151)) //<Brian Vaughn>//
- **[e12a9dfc9](facebook/react@e12a9dfc9 )**: Fix production-only updateSyncExternalStore() crash when doing setState in render ([#23150](facebook/react#23150)) //<Douglas Armstrong>//
- **[e48940255](facebook/react@e48940255 )**: Warn when a callback ref returns a function ([#23145](facebook/react#23145)) //<Dan Abramov>//
- **[d8cfeaf22](facebook/react@d8cfeaf22 )**: Fix context propagation for offscreen/fallback trees ([#23095](facebook/react#23095)) //<Dan Abramov>//
- **[d50482478](facebook/react@d50482478 )**: Enable scheduling profiler flag in react-dom/testing builds ([#23142](facebook/react#23142)) //<Brian Vaughn>//
- **[790b5246f](facebook/react@790b5246f )**: Fix setState ignored in Safari when iframe is added to DOM in the same commit ([#23111](facebook/react#23111)) //<Dan Abramov>//

Changelog:
[General][Changed] - React Native sync for revisions 51947a1...a3bde79

jest_e2e[run_all_tests]

Reviewed By: ShikaSD

Differential Revision: D34108924

fbshipit-source-id: 96acb66c1a7da79d6ef9403490cd0e29ad23d9fb
@gaearon gaearon mentioned this pull request Mar 29, 2022
zhengjitf pushed a commit to zhengjitf/react that referenced this pull request Apr 15, 2022
)

* [RFC] Add onHydrationError option to hydrateRoot

This is not the final API but I'm pushing it for discussion purposes.

When an error is thrown during hydration, we fallback to client
rendering, without triggering an error boundary. This is good because,
in many cases, the UI will recover and the user won't even notice that
something has gone wrong behind the scenes.

However, we shouldn't recover from these errors silently, because the
underlying cause might be pretty serious. Server-client mismatches are
not supposed to happen, even if UI doesn't break from the users
perspective. Ignoring them could lead to worse problems later. De-opting
from server to client rendering could also be a significant performance
regression, depending on the scope of the UI it affects.

So we need a way to log when hydration errors occur.

This adds a new option for `hydrateRoot` called `onHydrationError`. It's
symmetrical to the server renderer's `onError` option, and serves the
same purpose.

When no option is provided, the default behavior is to schedule a
browser task and rethrow the error. This will trigger the normal browser
behavior for errors, including dispatching an error event. If the app
already has error monitoring, this likely will just work as expected
without additional configuration.

However, we can also expose additional metadata about these errors, like
which Suspense boundaries were affected by the de-opt to client
rendering. (I have not exposed any metadata in this commit; API needs
more design work.)

There are other situations besides hydration where we recover from an
error without surfacing it to the user, or notifying an error boundary.
For example, if an error occurs during a concurrent render, it could be
due to a data race, so we try again synchronously in case that fixes it.
We should probably expose a way to log these types of errors, too. (Also
not implemented in this commit.)

* Log all recoverable errors

This expands the scope of onHydrationError to include all errors that
are not surfaced to the UI (an error boundary). In addition to errors
that occur during hydration, this also includes errors that recoverable
by de-opting to synchronous rendering. Typically (or really, by
definition) these errors are the result of a concurrent data race;
blocking the main thread fixes them by prevents subsequent races.

The logic for de-opting to synchronous rendering already existed. The
only thing that has changed is that we now log the errors instead of
silently proceeding.

The logging API has been renamed from onHydrationError
to onRecoverableError.

* Don't log recoverable errors until commit phase

If the render is interrupted and restarts, we don't want to log the
errors multiple times.

This change only affects errors that are recovered by de-opting to
synchronous rendering; we'll have to do something else for errors
during hydration, since they use a different recovery path.

* Only log hydration error if client render succeeds

Similar to previous step.

When an error occurs during hydration, we only want to log it if falling
back to client rendering _succeeds_. If client rendering fails,
the error will get reported to the nearest error boundary, so there's
no need for a duplicate log.

To implement this, I added a list of errors to the hydration context.
If the Suspense boundary successfully completes, they are added to
the main recoverable errors queue (the one I added in the
previous step.)

* Log error with queueMicrotask instead of Scheduler

If onRecoverableError is not provided, we default to rethrowing the
error in a separate task. Originally, I scheduled the task with
idle priority, but @sebmarkbage made the good point that if there are
multiple errors logs, we want to preserve the original order. So I've
switched it to a microtask. The priority can be lowered in userspace
by scheduling an additional task inside onRecoverableError.

* Only use host config method for default behavior

Redefines the contract of the host config's logRecoverableError method
to be a default implementation for onRecoverableError if a user-provided
one is not provided when the root is created.

* Log with reportError instead of rethrowing

In modern browsers, reportError will dispatch an error event, emulating
an uncaught JavaScript error. We can do this instead of rethrowing
recoverable errors in a microtask, which is nice because it avoids any
subtle ordering issues.

In older browsers and test environments, we'll fall back
to console.error.

* Naming nits

queueRecoverableHydrationErrors -> upgradeHydrationErrorsToRecoverable
nevilm-lt pushed a commit to nevilm-lt/react that referenced this pull request Apr 22, 2022
)

* [RFC] Add onHydrationError option to hydrateRoot

This is not the final API but I'm pushing it for discussion purposes.

When an error is thrown during hydration, we fallback to client
rendering, without triggering an error boundary. This is good because,
in many cases, the UI will recover and the user won't even notice that
something has gone wrong behind the scenes.

However, we shouldn't recover from these errors silently, because the
underlying cause might be pretty serious. Server-client mismatches are
not supposed to happen, even if UI doesn't break from the users
perspective. Ignoring them could lead to worse problems later. De-opting
from server to client rendering could also be a significant performance
regression, depending on the scope of the UI it affects.

So we need a way to log when hydration errors occur.

This adds a new option for `hydrateRoot` called `onHydrationError`. It's
symmetrical to the server renderer's `onError` option, and serves the
same purpose.

When no option is provided, the default behavior is to schedule a
browser task and rethrow the error. This will trigger the normal browser
behavior for errors, including dispatching an error event. If the app
already has error monitoring, this likely will just work as expected
without additional configuration.

However, we can also expose additional metadata about these errors, like
which Suspense boundaries were affected by the de-opt to client
rendering. (I have not exposed any metadata in this commit; API needs
more design work.)

There are other situations besides hydration where we recover from an
error without surfacing it to the user, or notifying an error boundary.
For example, if an error occurs during a concurrent render, it could be
due to a data race, so we try again synchronously in case that fixes it.
We should probably expose a way to log these types of errors, too. (Also
not implemented in this commit.)

* Log all recoverable errors

This expands the scope of onHydrationError to include all errors that
are not surfaced to the UI (an error boundary). In addition to errors
that occur during hydration, this also includes errors that recoverable
by de-opting to synchronous rendering. Typically (or really, by
definition) these errors are the result of a concurrent data race;
blocking the main thread fixes them by prevents subsequent races.

The logic for de-opting to synchronous rendering already existed. The
only thing that has changed is that we now log the errors instead of
silently proceeding.

The logging API has been renamed from onHydrationError
to onRecoverableError.

* Don't log recoverable errors until commit phase

If the render is interrupted and restarts, we don't want to log the
errors multiple times.

This change only affects errors that are recovered by de-opting to
synchronous rendering; we'll have to do something else for errors
during hydration, since they use a different recovery path.

* Only log hydration error if client render succeeds

Similar to previous step.

When an error occurs during hydration, we only want to log it if falling
back to client rendering _succeeds_. If client rendering fails,
the error will get reported to the nearest error boundary, so there's
no need for a duplicate log.

To implement this, I added a list of errors to the hydration context.
If the Suspense boundary successfully completes, they are added to
the main recoverable errors queue (the one I added in the
previous step.)

* Log error with queueMicrotask instead of Scheduler

If onRecoverableError is not provided, we default to rethrowing the
error in a separate task. Originally, I scheduled the task with
idle priority, but @sebmarkbage made the good point that if there are
multiple errors logs, we want to preserve the original order. So I've
switched it to a microtask. The priority can be lowered in userspace
by scheduling an additional task inside onRecoverableError.

* Only use host config method for default behavior

Redefines the contract of the host config's logRecoverableError method
to be a default implementation for onRecoverableError if a user-provided
one is not provided when the root is created.

* Log with reportError instead of rethrowing

In modern browsers, reportError will dispatch an error event, emulating
an uncaught JavaScript error. We can do this instead of rethrowing
recoverable errors in a microtask, which is nice because it avoids any
subtle ordering issues.

In older browsers and test environments, we'll fall back
to console.error.

* Naming nits

queueRecoverableHydrationErrors -> upgradeHydrationErrorsToRecoverable
shwanton pushed a commit to shwanton/react-native-macos that referenced this pull request Feb 13, 2023
Summary:
This sync includes the following changes:
- **[a3bde7974](facebook/react@a3bde7974 )**: Exclude react-dom/unstable_testing entry point from stable releases ([facebook#23258](facebook/react#23258)) //<Sebastian Markbåge>//
- **[569093276](facebook/react@569093276 )**: Add onErrorShell Callback ([facebook#23247](facebook/react#23247)) //<Sebastian Markbåge>//
- **[0dedfcc68](facebook/react@0dedfcc68 )**: Update the exports field ([facebook#23257](facebook/react#23257)) //<Sebastian Markbåge>//
- **[9d4e8e84f](facebook/react@9d4e8e84f )**: React Native raw event EventEmitter - intended for app-specific perf listeners and debugging ([facebook#23232](facebook/react#23232)) //<Joshua Gross>//
- **[1dece5235](facebook/react@1dece5235 )**: Add back warning with component stack on Hydration mismatch ([facebook#23241](facebook/react#23241)) //<salazarm>//
- **[cd4eb116c](facebook/react@cd4eb116c )**: Revert "update node.js version for CI ([facebook#23236](facebook/react#23236))" ([facebook#23239](facebook/react#23239)) //<dan>//
- **[1d7728bf9](facebook/react@1d7728bf9 )**: update node.js version for CI ([facebook#23236](facebook/react#23236)) //<sunderls>//
- **[848e802d2](facebook/react@848e802d2 )**: Add onRecoverableError option to hydrateRoot, createRoot ([facebook#23207](facebook/react#23207)) //<Andrew Clark>//
- **[5318971f5](facebook/react@5318971f5 )**: Remove logic for multiple error recovery attempts ([facebook#23227](facebook/react#23227)) //<Andrew Clark>//
- **[3a4462129](facebook/react@3a4462129 )**: Disable avoidThisFallback support in Fizz  ([facebook#23224](facebook/react#23224)) //<salazarm>//
- **[0318ac2c4](facebook/react@0318ac2c4 )**: Revert 4f5449 //<Ricky>//
- **[4f5449eb4](facebook/react@4f5449eb4 )**: Remove main from scheduler `index.js` //<Ricky>//
- **[3f5ff16c1](facebook/react@3f5ff16c1 )**: [Hydration] Fallback to client render if server rendered extra nodes ([facebook#23176](facebook/react#23176)) //<salazarm>//
- **[529dc3ce8](facebook/react@529dc3ce8 )**: Fix context providers in SSR when handling multiple requests ([facebook#23171](facebook/react#23171)) //<Fran Dios>//
- **[505c15c9e](facebook/react@505c15c9e )**: Don't inject timeline hooks unless React supports profiling ([facebook#23151](facebook/react#23151)) //<Brian Vaughn>//
- **[e12a9dfc9](facebook/react@e12a9dfc9 )**: Fix production-only updateSyncExternalStore() crash when doing setState in render ([facebook#23150](facebook/react#23150)) //<Douglas Armstrong>//
- **[e48940255](facebook/react@e48940255 )**: Warn when a callback ref returns a function ([facebook#23145](facebook/react#23145)) //<Dan Abramov>//
- **[d8cfeaf22](facebook/react@d8cfeaf22 )**: Fix context propagation for offscreen/fallback trees ([facebook#23095](facebook/react#23095)) //<Dan Abramov>//
- **[d50482478](facebook/react@d50482478 )**: Enable scheduling profiler flag in react-dom/testing builds ([facebook#23142](facebook/react#23142)) //<Brian Vaughn>//
- **[790b5246f](facebook/react@790b5246f )**: Fix setState ignored in Safari when iframe is added to DOM in the same commit ([facebook#23111](facebook/react#23111)) //<Dan Abramov>//

Changelog:
[General][Changed] - React Native sync for revisions 51947a1...a3bde79

jest_e2e[run_all_tests]

Reviewed By: ShikaSD

Differential Revision: D34108924

fbshipit-source-id: 96acb66c1a7da79d6ef9403490cd0e29ad23d9fb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants