[compiler][fixtures] Patch error-handling edge case in snap evaluator
Fix edge case in which we incorrectly returned a cached exception instead of trying to rerender with new props. ghstack-source-id: 843fb85df4a2ae7a88f296104fb16b5f9a34c76e Pull Request resolved: https://github.com/facebook/react/pull/31082
Mofei Zhang committed
Sep 30, 2024 at 12:24 UTC
2cbea245cca4044f02c4c231a7f86c8062074579
2 files changed
+32
-15
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/jump-unpoisoned/throw-before-scope-starts.expect.md
+1
-1
@@ -75,7 +75,7 @@ export const FIXTURE_ENTRYPOINT = {
75
76
### Eval output
77
(kind: ok) [[ (exception in render) Error: throw with error! ]]
78
-[[ (exception in render) Error: throw with error! ]]
78
+[2]
79
[[ (exception in render) Error: throw with error! ]]
80
[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]]
81
[null]
compiler/packages/snap/src/sprout/evaluator.ts
+31
-14
@@ -60,6 +60,7 @@ const ExportSchema = z.object({
60
FIXTURE_ENTRYPOINT: EntrypointSchema,
61
});
62
63
+const NO_ERROR_SENTINEL = Symbol();
64
/**
65
* Wraps WrapperTestComponent in an error boundary to simplify re-rendering
66
* when an exception is thrown.
@@ -67,31 +68,47 @@ const ExportSchema = z.object({
68
*/
69
class WrapperTestComponentWithErrorBoundary extends React.Component<
70
{fn: any; params: Array<any>},
70
- {hasError: boolean; error: any}
71
+ {errorFromLastRender: any}
72
> {
72
- propsErrorMap: MutableRefObject<Map<any, any>>;
73
+ /**
74
+ * Limit retries of the child component by caching seen errors.
75
+ */
76
+ propsErrorMap: Map<any, any>;
77
+ lastProps: any | null;
78
+ // lastProps: object | null;
79
constructor(props: any) {
80
super(props);
75
- this.state = {hasError: false, error: null};
76
- this.propsErrorMap = React.createRef() as MutableRefObject<Map<any, any>>;
77
- this.propsErrorMap.current = new Map();
81
+ this.lastProps = null;
82
+ this.propsErrorMap = new Map<any, any>();
83
+ this.state = {
84
+ errorFromLastRender: NO_ERROR_SENTINEL,
85
+ };
86
}
87
static getDerivedStateFromError(error: any) {
80
- return {hasError: true, error: error};
88
+ // Reschedule a second render that immediately returns the cached error
89
+ return {errorFromLastRender: error};
90
}
91
override componentDidUpdate() {
83
- if (this.state.hasError) {
84
- this.setState({hasError: false, error: null});
92
+ if (this.state.errorFromLastRender !== NO_ERROR_SENTINEL) {
93
+ // Reschedule a third render that immediately returns the cached error
94
+ this.setState({errorFromLastRender: NO_ERROR_SENTINEL});
95
}
96
}
97
override render() {
88
- if (this.state.hasError) {
89
- this.propsErrorMap.current!.set(
90
- this.props,
91
- `[[ (exception in render) ${this.state.error?.toString()} ]]`,
92
- );
98
+ if (
99
+ this.state.errorFromLastRender !== NO_ERROR_SENTINEL &&
100
+ this.props === this.lastProps
101
+ ) {
102
+ /**
103
+ * The last render errored, cache the error message to avoid running the
104
+ * test fixture more than once
105
+ */
106
+ const errorMsg = `[[ (exception in render) ${this.state.errorFromLastRender?.toString()} ]]`;
107
+ this.propsErrorMap.set(this.lastProps, errorMsg);
108
+ return errorMsg;
109
}
94
- const cachedError = this.propsErrorMap.current!.get(this.props);
110
+ this.lastProps = this.props;
111
+ const cachedError = this.propsErrorMap.get(this.props);
112
if (cachedError != null) {
113
return cachedError;
114
}