1+ import 'package:logging/logging.dart' ;
2+ import 'package:meta/meta.dart' ;
13import 'package:over_react/over_react.dart' ;
4+ import 'package:over_react/src/component/error_boundary_mixins.dart' ;
5+ import 'package:over_react/src/component/error_boundary_recoverable.dart' ;
26
37part 'error_boundary.over_react.g.dart' ;
48
@@ -22,4 +26,121 @@ class _$ErrorBoundaryState extends UiState with ErrorBoundaryStateMixin {}
2226
2327@Component 2(isWrapper: true , isErrorBoundary: true )
2428class ErrorBoundaryComponent <T extends ErrorBoundaryProps , S extends ErrorBoundaryState >
25- extends UiStatefulComponent2 <T , S > with ErrorBoundaryMixin <T , S > {}
29+ extends UiStatefulComponent2 <T , S > with ErrorBoundaryApi <T , S > {
30+
31+ // ---------------------------------------------- \/ ----------------------------------------------
32+ // How This ErrorBoundary Works:
33+ //
34+ // Background Info:
35+ // React gives each Error Boundary one chance to handle or recover when an error is throws, if
36+ // the Error Boundary's children throw again on its next render after React calls
37+ // `getDerivedStateFromError` and `componentDidCatch`, React will ascend the tree to the next
38+ // Error Boundary above it and rerender offering it a chance to handle the error. If none of
39+ // the parent Error Boundaries have a successful render cycle, React unmounts the entire tree.
40+ // __Note:__ When an Error Boundary remounts its children React clears all child components
41+ // previous state, including child ErrorBoundaries meaning they lose all previous knowledge
42+ // of any errors thrown.
43+ //
44+ // Solution:
45+ // To prevent unmounting the entire tree when React cannot find an Error Boundary that is able
46+ // to handle the error we wrap an Error Boundary with another Error Boundary (this one!). The
47+ // child Error Boundary will handle errors that are "recoverable", so if an error gets to this
48+ // Error Boundary we know it is "unrecoverable" and can present a fallback.
49+ //
50+ // -----------------------------------------------------------------------------------------------
51+ // Implementation:
52+ //
53+ // [1] Renders a child Error Boundary that is able to handle Errors thrown outside of the initial
54+ // render cycle, allowing it a chance to "recover".
55+ //
56+ // [2] If we catch an error in this Error Boundary that indicates that the child Error Boundary was
57+ // unable to handle or recover from the error, so we know that it was "unrecoverable" and we
58+ // haven't had a successful render there is never any DOM created that can used to display,
59+ // so we present an empty div instead.
60+ //
61+ // ---------------------------------------------- /\ ----------------------------------------------
62+
63+ @override
64+ get defaultProps => (newProps ()
65+ ..identicalErrorFrequencyTolerance = Duration (seconds: 5 )
66+ ..loggerName = defaultErrorBoundaryLoggerName
67+ ..shouldLogErrors = true
68+ );
69+
70+ @override
71+ get initialState => (newState ()
72+ ..hasError = false
73+ ..showFallbackUIOnError = true
74+ );
75+
76+ @override
77+ Map getDerivedStateFromError (error) => (newState ()
78+ ..hasError = true
79+ ..showFallbackUIOnError = true
80+ );
81+
82+ @override
83+ void componentDidCatch (error, ReactErrorInfo info) {
84+ if (props.onComponentDidCatch != null ) {
85+ props.onComponentDidCatch (error, info);
86+ }
87+
88+ _logErrorCaughtByErrorBoundary (error, info);
89+
90+ if (props.onComponentIsUnrecoverable != null ) {
91+ props.onComponentIsUnrecoverable (error, info);
92+ }
93+ }
94+
95+ @override
96+ render () {
97+ if (state.hasError) { // [2]
98+ return (Dom .div ()
99+ ..key = 'ohnoes'
100+ ..addTestId ('ErrorBoundary.unrecoverableErrorInnerHtmlContainerNode' )
101+ )();
102+ }
103+ return (RecoverableErrorBoundary ()
104+ ..addTestId ('RecoverableErrorBoundary' )
105+ ..modifyProps (addUnconsumedProps)
106+ )(props.children); // [1]
107+ }
108+
109+ @override
110+ void componentDidUpdate (Map prevProps, Map prevState, [dynamic snapshot]) {
111+ // If the child is different, and the error boundary is currently in an error state,
112+ // give the children a chance to mount.
113+ if (state.hasError) {
114+ final childThatCausedError = typedPropsFactory (prevProps).children.single;
115+ if (childThatCausedError != props.children.single) {
116+ reset ();
117+ }
118+ }
119+ }
120+
121+ /// Resets the [ErrorBoundary] to a non-error state.
122+ ///
123+ /// This can be called manually on the component instance using a `ref` -
124+ /// or by passing in a new child instance after a child has thrown an error.
125+ void reset () {
126+ setState (initialState);
127+ }
128+
129+ String get _loggerName {
130+ if (props.logger != null ) return props.logger.name;
131+
132+ return props.loggerName ?? defaultErrorBoundaryLoggerName;
133+ }
134+
135+ void _logErrorCaughtByErrorBoundary (
136+ /*Error|Exception*/ dynamic error,
137+ ReactErrorInfo info, {
138+ bool isRecoverable = true ,
139+ }) {
140+ if (! props.shouldLogErrors) return ;
141+
142+ final message = 'An unrecoverable error was caught by an ErrorBoundary (attempting to remount it was unsuccessful): \n Info: ${info .componentStack }' ;
143+
144+ (props.logger ?? Logger (_loggerName)).severe (message, error, info.dartStackTrace);
145+ }
146+ }
0 commit comments