From da7b4129dde7e5dc841a289f3e79bd21e3947bba Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 30 Sep 2024 13:54:04 -0700 Subject: [PATCH] address some feedabck --- example/suspense/suspense.dart | 3 +- lib/react.dart | 3 +- lib/react_client/react_interop.dart | 62 +------------------------- lib/src/react_client/lazy.dart | 68 +++++++++++++++++++++++++++++ test/react_lazy_test.dart | 40 +++++++++++++++++ test/react_suspense_test.dart | 5 +-- 6 files changed, 114 insertions(+), 67 deletions(-) create mode 100644 lib/src/react_client/lazy.dart diff --git a/example/suspense/suspense.dart b/example/suspense/suspense.dart index eacbf9c1..f92f356e 100644 --- a/example/suspense/suspense.dart +++ b/example/suspense/suspense.dart @@ -6,7 +6,6 @@ import 'dart:html'; import 'package:js/js.dart'; import 'package:react/hooks.dart'; import 'package:react/react.dart' as react; -import 'package:react/react_client/react_interop.dart'; import 'package:react/react_dom.dart' as react_dom; import './simple_component.dart' deferred as simple; @@ -16,7 +15,7 @@ main() { react_dom.render(content, querySelector('#content')); } -final lazyComponent = lazy(() async { +final lazyComponent = react.lazy(() async { await Future.delayed(Duration(seconds: 5)); await simple.loadLibrary(); diff --git a/lib/react.dart b/lib/react.dart index a2979151..d7884ed6 100644 --- a/lib/react.dart +++ b/lib/react.dart @@ -21,7 +21,8 @@ import 'package:react/src/react_client/private_utils.dart' show validateJsApi, v export 'package:react/src/context.dart'; export 'package:react/src/prop_validator.dart'; export 'package:react/src/react_client/event_helpers.dart'; -export 'package:react/react_client/react_interop.dart' show forwardRef2, createRef, memo2, lazy; +export 'package:react/react_client/react_interop.dart' show forwardRef2, createRef, memo2; +export 'package:react/src/react_client/lazy.dart' show lazy; export 'package:react/src/react_client/synthetic_event_wrappers.dart' hide NonNativeDataTransfer; export 'package:react/src/react_client/synthetic_data_transfer.dart' show SyntheticDataTransfer; diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index 764f8db2..61d9af8a 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -18,8 +18,7 @@ import 'package:react/react_client/js_backed_map.dart'; import 'package:react/react_client/component_factory.dart' show ReactDartWrappedComponentFactoryProxy; import 'package:react/src/react_client/dart2_interop_workaround_bindings.dart'; import 'package:react/src/typedefs.dart'; - -import '../src/js_interop_util.dart'; +import 'package:react/src/js_interop_util.dart'; typedef ReactJsComponentFactory = ReactElement Function(dynamic props, dynamic children); @@ -277,65 +276,6 @@ ReactComponentFactoryProxy memo2(ReactComponentFactoryProxy factory, return ReactDartWrappedComponentFactoryProxy(hoc); } -/// Defer loading a component's code until it is rendered for the first time. -/// -/// The `lazy` function is used to create lazy components in react-dart. Lazy components are able to run asynchronous code only when they are trying to be rendered for the first time, allowing for deferred loading of the component's code. -/// -/// To use the `lazy` function, you need to wrap the lazy component with a `Suspense` component. The `Suspense` component allows you to specify what should be displayed while the lazy component is loading, such as a loading spinner or a placeholder. -/// -/// Example usage: -/// ```dart -/// import 'package:react/react.dart' show lazy, Suspense; -/// import './simple_component.dart' deferred as simple; -/// -/// final lazyComponent = lazy(() async { -/// await simple.loadLibrary(); -/// return simple.SimpleComponent; -/// }); -/// -/// // Wrap the lazy component with Suspense -/// final app = Suspense( -/// { -/// fallback: 'Loading...', -/// }, -/// lazyComponent({}), -/// ); -/// ``` -/// -/// Defer loading a component’s code until it is rendered for the first time. -/// -/// Lazy components need to be wrapped with `Suspense` to render. -/// `Suspense` also allows you to specify what should be displayed while the lazy component is loading. -ReactComponentFactoryProxy lazy(Future Function() load) { - final hoc = React.lazy( - allowInterop( - () => futureToPromise( - (() async { - final factory = await load(); - // By using a wrapper uiForwardRef it ensures that we have a matching factory proxy type given to react-dart's lazy, - // a `ReactDartWrappedComponentFactoryProxy`. This is necessary to have consistent prop conversions since we don't - // have access to the original factory proxy outside of this async block. - final wrapper = forwardRef2((props, ref) { - final children = props['children']; - return factory.build( - {...props, 'ref': ref}, - [ - if (children != null && !(children is List && children.isEmpty)) children, - ], - ); - }); - return jsify({'default': wrapper.type}); - })(), - ), - ), - ); - - // Setting this version and wrapping with ReactDartWrappedComponentFactoryProxy - // is only okay because it matches the version and factory proxy of the wrapperFactory above. - setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2); - return ReactDartWrappedComponentFactoryProxy(hoc); -} - abstract class ReactDom { static Element? findDOMNode(ReactNode object) => ReactDOM.findDOMNode(object); static dynamic render(ReactNode component, Element element) => ReactDOM.render(component, element); diff --git a/lib/src/react_client/lazy.dart b/lib/src/react_client/lazy.dart new file mode 100644 index 00000000..c79bbaa5 --- /dev/null +++ b/lib/src/react_client/lazy.dart @@ -0,0 +1,68 @@ + +import 'dart:js'; +import 'dart:js_util'; + +import 'package:react/react.dart'; +import 'package:react/react_client/component_factory.dart'; +import 'package:react/react_client/react_interop.dart'; +import 'package:react/src/js_interop_util.dart'; + +/// Defer loading a component's code until it is rendered for the first time. +/// +/// The `lazy` function is used to create lazy components in react-dart. Lazy components are able to run asynchronous code only when they are trying to be rendered for the first time, allowing for deferred loading of the component's code. +/// +/// To use the `lazy` function, you need to wrap the lazy component with a `Suspense` component. The `Suspense` component allows you to specify what should be displayed while the lazy component is loading, such as a loading spinner or a placeholder. +/// +/// Example usage: +/// ```dart +/// import 'package:react/react.dart' show lazy, Suspense; +/// import './simple_component.dart' deferred as simple; +/// +/// final lazyComponent = lazy(() async { +/// await simple.loadLibrary(); +/// return simple.SimpleComponent; +/// }); +/// +/// // Wrap the lazy component with Suspense +/// final app = Suspense( +/// { +/// fallback: 'Loading...', +/// }, +/// lazyComponent({}), +/// ); +/// ``` +/// +/// Defer loading a component’s code until it is rendered for the first time. +/// +/// Lazy components need to be wrapped with `Suspense` to render. +/// `Suspense` also allows you to specify what should be displayed while the lazy component is loading. +ReactComponentFactoryProxy lazy(Future Function() load) { + final hoc = React.lazy( + allowInterop( + () => futureToPromise( + (() async { + final factory = await load(); + // By using a wrapper uiForwardRef it ensures that we have a matching factory proxy type given to react-dart's lazy, + // a `ReactDartWrappedComponentFactoryProxy`. This is necessary to have consistent prop conversions since we don't + // have access to the original factory proxy outside of this async block. + final wrapper = forwardRef2((props, ref) { + final children = props['children']; + return factory.build( + {...props, 'ref': ref}, + [ + if (children != null && !(children is List && children.isEmpty)) children, + ], + ); + }); + return jsify({'default': wrapper.type}); + })(), + ), + ), + ); + + // Setting this version and wrapping with ReactDartWrappedComponentFactoryProxy + // is only okay because it matches the version and factory proxy of the wrapperFactory above. + // ignore: invalid_use_of_protected_member + setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2); + return ReactDartWrappedComponentFactoryProxy(hoc); +} diff --git a/test/react_lazy_test.dart b/test/react_lazy_test.dart index 2d87e3dd..635d1d2b 100644 --- a/test/react_lazy_test.dart +++ b/test/react_lazy_test.dart @@ -2,11 +2,13 @@ @JS() library react.react_lazy_test; +import 'dart:async'; import 'dart:js_util'; import 'package:js/js.dart'; import 'package:react/hooks.dart'; import 'package:react/react.dart' as react; +import 'package:react/react_test_utils.dart' as rtu; import 'package:react/react_client/component_factory.dart'; import 'package:react/react_client/react_interop.dart'; import 'package:test/test.dart'; @@ -15,6 +17,24 @@ import 'factory/common_factory_tests.dart'; main() { group('lazy', () { + // Event more lazy behavior is tested in `react_suspense_test.dart` + + test('correctly throws errors from within load function to the closest error boundary', () async { + const errorString = 'intentional future error'; + final errors = []; + final errorCompleter = Completer(); + final ThrowingLazyTest = react.lazy(() async { throw Exception(errorString);}); + onError(error, info) { + errors.add([error, info]); + errorCompleter.complete(); + } + expect(() => rtu.renderIntoDocument(_ErrorBoundary({'onComponentDidCatch': onError}, react.Suspense({'fallback': 'Loading...'}, ThrowingLazyTest({})))), returnsNormally); + await expectLater(errorCompleter.future, completes); + expect(errors, hasLength(1)); + expect(errors.first.first, isA().having((e) => e.toString(), 'message', contains(errorString))); + expect(errors.first.last, isA()); + }); + group('Dart component', () { final LazyTest = react.lazy(() async => react.forwardRef2((props, ref) { useImperativeHandle(ref, () => TestImperativeHandle()); @@ -77,3 +97,23 @@ class TestImperativeHandle {} @JS() external ReactClass get _JsFoo; + +final _ErrorBoundary = react.registerComponent2(() => _ErrorBoundaryComponent(), skipMethods: []); + +class _ErrorBoundaryComponent extends react.Component2 { + @override + get initialState => {'hasError': false}; + + @override + getDerivedStateFromError(dynamic error) => {'hasError': true}; + + @override + componentDidCatch(dynamic error, ReactErrorInfo info) { + props['onComponentDidCatch'](error, info); + } + + @override + render() { + return (state['hasError'] as bool) ? null : props['children']; + } +} diff --git a/test/react_suspense_test.dart b/test/react_suspense_test.dart index b2f7c4bb..ec6b1c9b 100644 --- a/test/react_suspense_test.dart +++ b/test/react_suspense_test.dart @@ -6,7 +6,6 @@ import 'dart:html'; import 'package:js/js.dart'; import 'package:react/react.dart' as react; -import 'package:react/react_client/react_interop.dart'; import 'package:react/react_dom.dart' as react_dom; import 'package:test/test.dart'; @@ -15,7 +14,7 @@ import './react_suspense_lazy_component.dart' deferred as simple; main() { group('Suspense', () { test('renders fallback UI first followed by the real component', () async { - final lazyComponent = lazy(() async { + final lazyComponent = react.lazy(() async { await simple.loadLibrary(); await Future.delayed(Duration(seconds: 1)); return simple.SimpleFunctionComponent; @@ -48,7 +47,7 @@ main() { }); test('is instant after the lazy component has been loaded once', () async { - final lazyComponent = lazy(() async { + final lazyComponent = react.lazy(() async { await simple.loadLibrary(); await Future.delayed(Duration(seconds: 1)); return simple.SimpleFunctionComponent;