|
| 1 | +import 'dart:html'; |
| 2 | +import 'dart:js_util' as js_util; |
| 3 | + |
| 4 | +import 'package:js/js.dart'; |
1 | 5 | import 'package:meta/meta.dart'; |
2 | 6 | import 'package:over_react/over_react.dart'; |
| 7 | +import 'package:react/react_client.dart'; |
| 8 | +import 'package:react/react_client/js_interop_helpers.dart'; |
| 9 | +import 'package:react/react_client/react_interop.dart' show React, ReactClassConfig, throwErrorFromJS; |
3 | 10 |
|
4 | 11 | // ignore: uri_has_not_been_generated |
5 | 12 | part 'error_boundary.over_react.g.dart'; |
6 | 13 |
|
| 14 | +/// A __temporary, private JS component for use only by [ErrorBoundary]__ that utilizes its own lightweight |
| 15 | +/// JS interop to make use of the ReactJS 16 `componentDidCatch` lifecycle method to prevent consumer |
| 16 | +/// react component trees from unmounting as a result of child component errors being "uncaught". |
| 17 | +/// |
| 18 | +/// > __Why this is here__ |
| 19 | +/// > |
| 20 | +/// > In order to release react-dart 5.0.0 _(which upgrades to ReactJS 16)_ |
| 21 | +/// without depending on Dart 2 / `Component2` (coming in react-dart 5.1.0) / `UiComponent2` (coming in over_react 3.1.0) - |
| 22 | +/// and all the new lifecycle methods that those expose, we need to ensure that - at a minimum - the `componentDidCatch` |
| 23 | +/// lifecycle method is handled by components wrapped in our [ErrorBoundary] component so that the behavior of |
| 24 | +/// an application when a component within a tree throws - is the same as it was when using ReactJS 15. |
| 25 | +/// > |
| 26 | +/// > Otherwise, the update to react-dart 5.0.0 / over_react 3.0.0 will result in consumer apps rendering completely |
| 27 | +/// "blank" screens when their trees unmount as a result of a child component throwing an error. |
| 28 | +/// This would be unexpected, unwanted - and since we will not add a Dart js-interop layer around `componentDidCatch` |
| 29 | +/// until react-dart 5.1.0 / over_react 3.1.0 - unmanageable for consumers. |
| 30 | +/// |
| 31 | +/// __This will be removed in over_react 3.1.0__ once [ErrorBoundaryComponent] is extending from `UiStatefulComponent2` |
| 32 | +/// which will ensure that the [ErrorBoundaryComponent.componentDidCatch] lifecycle method has real js-interop bindings |
| 33 | +/// via react-dart 5.1.0's `Component2` base class. |
| 34 | +/// |
| 35 | +/// TODO: Remove in 3.1.0 |
| 36 | +final ReactElement Function([Map props, List children]) _jsErrorBoundaryComponentFactory = (() { |
| 37 | + var componentClass = React.createClass(jsifyAndAllowInterop({ |
| 38 | + 'displayName': 'JsErrorBoundary', |
| 39 | + 'render': allowInteropCaptureThis((jsThis) { |
| 40 | + final jsProps = js_util.getProperty(jsThis, 'props'); |
| 41 | + return js_util.getProperty(jsProps, 'children'); |
| 42 | + }), |
| 43 | + 'componentDidCatch': allowInteropCaptureThis((jsThis, error, info) { |
| 44 | + final jsProps = js_util.getProperty(jsThis, 'props'); |
| 45 | + // Due to the error object being passed in from ReactJS it is a javascript object that does not get dartified. |
| 46 | + // To fix this we throw the error again from Dart to the JS side and catch it Dart side which re-dartifies it. |
| 47 | + try { |
| 48 | + throwErrorFromJS(error); |
| 49 | + } catch (error, stack) { |
| 50 | + final callback = js_util.getProperty(jsProps, 'onComponentDidCatch'); |
| 51 | + |
| 52 | + if (callback != null) { |
| 53 | + callback(error, info); |
| 54 | + } |
| 55 | + } |
| 56 | + }), |
| 57 | + })); |
| 58 | + |
| 59 | + // Despite what the ReactJS docs say about only needing _either_ componentDidCatch or getDerivedStateFromError |
| 60 | + // in order to define an "error boundary" component, that is not actually the case. |
| 61 | + // |
| 62 | + // The tree will never get re-rendered after an error is caught unless both are defined. |
| 63 | + // ignore: argument_type_not_assignable |
| 64 | + js_util.setProperty(componentClass, 'getDerivedStateFromError', allowInterop((_) => js_util.newObject())); |
| 65 | + |
| 66 | + var reactFactory = React.createFactory(componentClass); |
| 67 | + |
| 68 | + return ([Map props = const {}, List children = const []]) { |
| 69 | + return reactFactory(jsifyAndAllowInterop(props), listifyChildren(children)); |
| 70 | + }; |
| 71 | +})(); |
| 72 | + |
7 | 73 | // TODO: Need to type the second argument once react-dart implements bindings for the ReactJS "componentStack". |
8 | | -typedef _ComponentDidCatchCallback(Error error, /*ComponentStack*/dynamic componentStack); |
| 74 | +typedef _ComponentDidCatchCallback(/*Error*/dynamic error, /*ComponentStack*/dynamic componentStack); |
9 | 75 |
|
10 | 76 | // TODO: Need to type the second argument once react-dart implements bindings for the ReactJS "componentStack". |
11 | | -typedef ReactElement _FallbackUiRenderer(Error error, /*ComponentStack*/dynamic componentStack); |
| 77 | +typedef ReactElement _FallbackUiRenderer(/*Error*/dynamic error, /*ComponentStack*/dynamic componentStack); |
12 | 78 |
|
13 | 79 | /// A higher-order component that will catch ReactJS errors anywhere within the child component tree and |
14 | 80 | /// display a fallback UI instead of the component tree that crashed. |
@@ -93,12 +159,19 @@ class ErrorBoundaryComponent<T extends ErrorBoundaryProps, S extends ErrorBounda |
93 | 159 | } |
94 | 160 |
|
95 | 161 | @override |
96 | | - render() => state.hasError |
97 | | - ? props.fallbackUIRenderer(_error, _componentStack) |
98 | | - : props.children.single; |
| 162 | + render() { |
| 163 | + // TODO: 3.1.0 - Remove the `_jsErrorBoundaryComponentFactory`, and restore just the children of it once this component is extending from `UiStatefulComponent2`. |
| 164 | + return _jsErrorBoundaryComponentFactory({ |
| 165 | + 'onComponentDidCatch': props.onComponentDidCatch |
| 166 | + }, |
| 167 | + state.hasError |
| 168 | + ? [props.fallbackUIRenderer(_error, _componentStack)] |
| 169 | + : props.children |
| 170 | + ); |
| 171 | + } |
99 | 172 |
|
100 | 173 | ReactElement _renderDefaultFallbackUI(_, __) => |
101 | | - throw new UnimplementedError('Fallback UI will not be supported until support for ReactJS 16 is released in version 3.0.0'); |
| 174 | + throw new UnimplementedError('Fallback UI will not be supported until support for ReactJS 16 lifecycle methods is released in version 3.1.0'); |
102 | 175 |
|
103 | 176 | @mustCallSuper |
104 | 177 | @override |
|
0 commit comments