Skip to content

Commit

Permalink
Merge pull request #298 from Workiva/3.1.0/CPLAT-5037_finish-error-bo…
Browse files Browse the repository at this point in the history
…undary-bindings

CPLAT-5037 Finish error boundary componentry
  • Loading branch information
rmconsole5-wk authored May 28, 2019
2 parents 5798f60 + f1d27e9 commit 51b4d24
Show file tree
Hide file tree
Showing 13 changed files with 831 additions and 517 deletions.
2 changes: 2 additions & 0 deletions lib/over_react.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ export 'package:react/react.dart' show
export 'package:react/src/react_client/js_backed_map.dart' show JsBackedMap;

export 'package:react/react_client.dart' show setClientConfiguration, ReactElement, ReactComponentFactoryProxy;
export 'package:react/react_client/react_interop.dart' show ReactErrorInfo;

export 'src/component/abstract_transition.dart';
export 'src/component/abstract_transition_props.dart';
export 'src/component/aria_mixin.dart';
export 'src/component/callback_typedefs.dart';
export 'src/component/error_boundary.dart';
export 'src/component/error_boundary_mixins.dart';
export 'src/component/dom_components.dart';
export 'src/component/dummy_component.dart';
export 'src/component/prop_mixins.dart';
Expand Down
175 changes: 7 additions & 168 deletions lib/src/component/error_boundary.dart
Original file line number Diff line number Diff line change
@@ -1,186 +1,25 @@
import 'dart:html';
import 'dart:js_util' as js_util;

import 'package:js/js.dart';
import 'package:meta/meta.dart';
import 'package:over_react/over_react.dart';
import 'package:react/react_client.dart';
import 'package:react/react_client/js_interop_helpers.dart';
import 'package:react/react_client/react_interop.dart' show React, ReactClassConfig, throwErrorFromJS;

part 'error_boundary.over_react.g.dart';

/// A __temporary, private JS component for use only by [ErrorBoundary]__ that utilizes its own lightweight
/// JS interop to make use of the ReactJS 16 `componentDidCatch` lifecycle method to prevent consumer
/// react component trees from unmounting as a result of child component errors being "uncaught".
///
/// > __Why this is here__
/// >
/// > In order to release react-dart 5.0.0 _(which upgrades to ReactJS 16)_
/// without depending on Dart 2 / `Component2` (coming in react-dart 5.1.0) / `UiComponent2` (coming in over_react 3.1.0) -
/// and all the new lifecycle methods that those expose, we need to ensure that - at a minimum - the `componentDidCatch`
/// lifecycle method is handled by components wrapped in our [ErrorBoundary] component so that the behavior of
/// an application when a component within a tree throws - is the same as it was when using ReactJS 15.
/// >
/// > Otherwise, the update to react-dart 5.0.0 / over_react 3.0.0 will result in consumer apps rendering completely
/// "blank" screens when their trees unmount as a result of a child component throwing an error.
/// This would be unexpected, unwanted - and since we will not add a Dart js-interop layer around `componentDidCatch`
/// until react-dart 5.1.0 / over_react 3.1.0 - unmanageable for consumers.
///
/// __This will be removed in over_react 3.1.0__ once [ErrorBoundaryComponent] is extending from `UiStatefulComponent2`
/// which will ensure that the [ErrorBoundaryComponent.componentDidCatch] lifecycle method has real js-interop bindings
/// via react-dart 5.1.0's `Component2` base class.
///
/// TODO: Remove in 3.1.0
final ReactElement Function([Map props, List children]) _jsErrorBoundaryComponentFactory = (() {
var componentClass = React.createClass(jsifyAndAllowInterop({
'displayName': 'JsErrorBoundary',
'render': allowInteropCaptureThis((jsThis) {
final jsProps = js_util.getProperty(jsThis, 'props');
return js_util.getProperty(jsProps, 'children');
}),
'componentDidCatch': allowInteropCaptureThis((jsThis, error, info) {
final jsProps = js_util.getProperty(jsThis, 'props');
// Due to the error object being passed in from ReactJS it is a javascript object that does not get dartified.
// To fix this we throw the error again from Dart to the JS side and catch it Dart side which re-dartifies it.
try {
throwErrorFromJS(error);
} catch (error, stack) {
final callback = js_util.getProperty(jsProps, 'onComponentDidCatch');

if (callback != null) {
callback(error, info);
}
}
}),
}));

// Despite what the ReactJS docs say about only needing _either_ componentDidCatch or getDerivedStateFromError
// in order to define an "error boundary" component, that is not actually the case.
//
// The tree will never get re-rendered after an error is caught unless both are defined.
// ignore: argument_type_not_assignable
js_util.setProperty(componentClass, 'getDerivedStateFromError', allowInterop((_) => js_util.newObject()));

var reactFactory = React.createFactory(componentClass);

return ([Map props = const {}, List children = const []]) {
return reactFactory(jsifyAndAllowInterop(props), listifyChildren(children));
};
})();

// TODO: Need to type the second argument once react-dart implements bindings for the ReactJS "componentStack".
typedef _ComponentDidCatchCallback(/*Error*/dynamic error, /*ComponentStack*/dynamic componentStack);

// TODO: Need to type the second argument once react-dart implements bindings for the ReactJS "componentStack".
typedef ReactElement _FallbackUiRenderer(/*Error*/dynamic error, /*ComponentStack*/dynamic componentStack);

/// A higher-order component that will catch ReactJS errors anywhere within the child component tree and
/// display a fallback UI instead of the component tree that crashed.
///
/// Optionally, use the [ErrorBoundaryProps.onComponentDidCatch]
/// to send error / stack trace information to a logging endpoint for your application.
///
/// > __NOTE: This component does not yet do any of this__.
/// >
/// > It will begin providing the boundary / fallback UI behavior once support
/// for ReactJS 16 is released in over_react version 3.0.0
/// To make your own custom error boundaries, you can utilize the [ErrorBoundaryPropsMixin],
/// [ErrorBoundaryStateMixin] and [ErrorBoundaryMixin]s on any component that is annotated
/// using `@Component2(isErrorBoundary: true)`. See the [ErrorBoundaryMixin] for an example implementation.
@Factory()
UiFactory<ErrorBoundaryProps> ErrorBoundary = _$ErrorBoundary;

@Props()
class _$ErrorBoundaryProps extends UiProps {
/// An optional callback that will be called with an [Error] and a `ComponentStack`
/// containing information about which component in the tree threw the error when
/// the `componentDidCatch` lifecycle method is called.
///
/// This callback can be used to log component errors like so:
///
/// (ErrorBoundary()
/// ..onComponentDidCatch = (error, componentStack) {
/// // It is up to you to implement the service / thing that calls the service.
/// logComponentStackToAService(error, componentStack);
/// }
/// )(
/// // The rest of your component tree
/// )
///
/// > See: <https://reactjs.org/docs/react-component.html#componentdidcatch>
_ComponentDidCatchCallback onComponentDidCatch;

/// A renderer that will be used to render "fallback" UI instead of the child
/// component tree that crashed.
///
/// > Default: [ErrorBoundaryComponent._renderDefaultFallbackUI]
_FallbackUiRenderer fallbackUIRenderer;
}
class _$ErrorBoundaryProps extends UiProps with ErrorBoundaryPropsMixin {}

@State()
class _$ErrorBoundaryState extends UiState {
/// Whether the tree that the [ErrorBoundary] is wrapping around threw an error.
///
/// When `true`, fallback UI will be rendered using [ErrorBoundaryProps.fallbackUIRenderer].
bool hasError;
}
class _$ErrorBoundaryState extends UiState with ErrorBoundaryStateMixin {}

@Component(isWrapper: true)
@Component2(isWrapper: true, isErrorBoundary: true)
class ErrorBoundaryComponent<T extends ErrorBoundaryProps, S extends ErrorBoundaryState>
extends UiStatefulComponent<T, S> {
Error _error;
/*ComponentStack*/dynamic _componentStack;

@override
Map getDefaultProps() => (newProps()
..fallbackUIRenderer = _renderDefaultFallbackUI
);

@override
Map getInitialState() => (newState()
..hasError = false
);

@mustCallSuper
/*@override*/
S getDerivedStateFromError(_) {
return newState()..hasError = true;
}

@mustCallSuper
/*@override*/
void componentDidCatch(Error error, /*ComponentStack*/dynamic componentStack) {
_error = error;
_componentStack = componentStack;

if (props.onComponentDidCatch != null) {
props.onComponentDidCatch(error, componentStack);
}
}

@override
render() {
// TODO: 3.1.0 - Remove the `_jsErrorBoundaryComponentFactory`, and restore just the children of it once this component is extending from `UiStatefulComponent2`.
return _jsErrorBoundaryComponentFactory({
'onComponentDidCatch': props.onComponentDidCatch
},
state.hasError
? [props.fallbackUIRenderer(_error, _componentStack)]
: props.children
);
}

ReactElement _renderDefaultFallbackUI(_, __) =>
throw new UnimplementedError('Fallback UI will not be supported until support for ReactJS 16 lifecycle methods is released in version 3.1.0');

@mustCallSuper
@override
void validateProps([Map appliedProps]) {
super.validateProps(appliedProps);
final children = domProps(appliedProps).children;

if (children.length != 1) {
throw new PropError.value(children, 'children', 'ErrorBoundary accepts only a single child.');
} else if (!isValidElement(children.single)) {
throw new PropError.value(children, 'children', 'ErrorBoundary accepts only a single ReactComponent child.');
}
}
}
extends UiStatefulComponent2<T, S> with ErrorBoundaryMixin<T, S> {}
Loading

0 comments on commit 51b4d24

Please sign in to comment.