From 4a05523661145dd8b221e7bbe3246526b2edf325 Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 9 Sep 2024 12:24:25 -0700 Subject: [PATCH 01/14] add lazy implementation and tests, update old usages of lazy --- example/suspense/suspense.dart | 37 +++++++----------------- lib/react.dart | 2 +- lib/react_client/react_interop.dart | 40 ++++++++++++++++++++++++++ test/factory/common_factory_tests.dart | 10 +++---- test/react_lazy_test.dart | 26 +++++++++++++++++ test/react_lazy_test.html | 12 ++++++++ test/react_suspense_test.dart | 24 ---------------- 7 files changed, 95 insertions(+), 56 deletions(-) create mode 100644 test/react_lazy_test.dart create mode 100644 test/react_lazy_test.html diff --git a/example/suspense/suspense.dart b/example/suspense/suspense.dart index 7517a647..eacbf9c1 100644 --- a/example/suspense/suspense.dart +++ b/example/suspense/suspense.dart @@ -1,37 +1,15 @@ @JS() -library js_components; +library example.suspense.suspense; import 'dart:html'; -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_client.dart'; import 'package:react/react_client/react_interop.dart'; import 'package:react/react_dom.dart' as react_dom; -import 'package:react/src/js_interop_util.dart'; import './simple_component.dart' deferred as simple; -@JS('React.lazy') -external ReactClass jsLazy(Promise Function() factory); - -// Only intended for testing purposes, Please do not copy/paste this into repo. -// This will most likely be added to the PUBLIC api in the future, -// but needs more testing and Typing decisions to be made first. -ReactJsComponentFactoryProxy lazy(Future Function() factory) => - ReactJsComponentFactoryProxy( - jsLazy( - allowInterop( - () => futureToPromise( - // React.lazy only supports "default exports" from a module. - // This `{default: yourExport}` workaround can be found in the React.lazy RFC comments. - // See: https://github.com/reactjs/rfcs/pull/64#issuecomment-431507924 - (() async => jsify({'default': (await factory()).type}))(), - ), - ), - ), - ); - main() { final content = wrapper({}); @@ -39,17 +17,24 @@ main() { } final lazyComponent = lazy(() async { - await simple.loadLibrary(); await Future.delayed(Duration(seconds: 5)); + await simple.loadLibrary(); + return simple.SimpleComponent; }); var wrapper = react.registerFunctionComponent(WrapperComponent, displayName: 'wrapper'); WrapperComponent(Map props) { + final showComponent = useState(false); return react.div({ 'id': 'lazy-wrapper' }, [ - react.Suspense({'fallback': 'Loading...'}, [lazyComponent({})]) + react.button({ + 'onClick': (_) { + showComponent.set(!showComponent.value); + } + }, 'Toggle component'), + react.Suspense({'fallback': 'Loading...'}, showComponent.value ? lazyComponent({}) : null) ]); } diff --git a/lib/react.dart b/lib/react.dart index c4596e70..a2979151 100644 --- a/lib/react.dart +++ b/lib/react.dart @@ -21,7 +21,7 @@ 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; +export 'package:react/react_client/react_interop.dart' show forwardRef2, createRef, memo2, 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 9ded7771..c68dc46e 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -19,6 +19,8 @@ import 'package:react/react_client/component_factory.dart' show ReactDartWrapped import 'package:react/src/react_client/dart2_interop_workaround_bindings.dart'; import 'package:react/src/typedefs.dart'; +import '../src/js_interop_util.dart'; + typedef ReactJsComponentFactory = ReactElement Function(dynamic props, dynamic children); // ---------------------------------------------------------------------------- @@ -42,6 +44,7 @@ abstract class React { dynamic wrapperFunction, [ bool Function(JsMap prevProps, JsMap nextProps)? areEqual, ]); + external static ReactClass lazy(Promise Function() loadFunction); external static bool isValidElement(dynamic object); @@ -274,6 +277,43 @@ 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 loaded only when they are rendered for the first time, which can help improve the initial loading time of your application. +/// +/// To use the `lazy` function, you need to wrap the lazy component with a `Suspense` widget. The `Suspense` widget 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_client.dart' show lazy, Suspense; +/// +/// final lazyComponent = lazy(() => import('./path/to/lazy_component.dart')); +/// +/// // Wrap the lazy component with Suspense +/// final app = Suspense( +/// { +/// fallback: 'Loading...', +/// }, +/// lazyComponent(), +/// ); +/// ``` +/// +/// Note: The `lazy` function is part of the `react_client` library in React Dart. +/// 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() loadFunction) { + final hoc = React.lazy(allowInterop(() => futureToPromise((() async { + final factory = await loadFunction(); + return jsify({'default': factory.type}); + })()))); + 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/test/factory/common_factory_tests.dart b/test/factory/common_factory_tests.dart index 57f90307..66ee402e 100644 --- a/test/factory/common_factory_tests.dart +++ b/test/factory/common_factory_tests.dart @@ -26,10 +26,10 @@ import '../util.dart'; /// [dartComponentVersion] should be specified for all components with Dart render code in order to /// properly test `props.children`, forwardRef compatibility, etc. void commonFactoryTests(ReactComponentFactoryProxy factory, - {String? dartComponentVersion, bool skipPropValuesTest = false}) { + {String? dartComponentVersion, bool skipPropValuesTest = false, ReactElement Function(dynamic children)? renderWrapper}) { _childKeyWarningTests( factory, - renderWithUniqueOwnerName: _renderWithUniqueOwnerName, + renderWithUniqueOwnerName: (ReactElement Function() render) => _renderWithUniqueOwnerName(render, renderWrapper), ); test('renders an instance with the corresponding `type`', () { @@ -532,7 +532,7 @@ void _childKeyWarningTests(ReactComponentFactoryProxy factory, }); test('warns when a single child is passed as a list', () { - _renderWithUniqueOwnerName(() => factory({}, [react.span({})])); + renderWithUniqueOwnerName(() => factory({}, [react.span({})])); expect(consoleErrorCalled, isTrue, reason: 'should have outputted a warning'); expect(consoleErrorMessage, contains('Each child in a list should have a unique "key" prop.')); @@ -577,12 +577,12 @@ int _nextFactoryId = 0; /// Renders the provided [render] function with a Component2 owner that will have a unique name. /// /// This prevents React JS from not printing key warnings it deems as "duplicates". -void _renderWithUniqueOwnerName(ReactElement Function() render) { +void _renderWithUniqueOwnerName(ReactElement Function() render, [ReactElement Function(dynamic)? wrapper]) { final factory = react.registerComponent2(() => _UniqueOwnerHelperComponent()); factory.reactClass.displayName = 'OwnerHelperComponent_$_nextFactoryId'; _nextFactoryId++; - rtu.renderIntoDocument(factory({'render': render})); + rtu.renderIntoDocument(factory({'render': wrapper != null ? () => wrapper(render()) : render})); } class _UniqueOwnerHelperComponent extends react.Component2 { diff --git a/test/react_lazy_test.dart b/test/react_lazy_test.dart new file mode 100644 index 00000000..c903a8b8 --- /dev/null +++ b/test/react_lazy_test.dart @@ -0,0 +1,26 @@ +@TestOn('browser') +library react.react_lazy_test; + +import 'package:react/react.dart' as react; +import 'package:react/react_client/react_interop.dart'; +import 'package:test/test.dart'; + +import 'factory/common_factory_tests.dart'; + +main() { + group('lazy', () { + group('- common factory behavior -', () { + final LazyTest = react.lazy(() async => react.registerFunctionComponent((props) { + props['onDartRender']?.call(props); + return react.div({...props}); + })); + + commonFactoryTests( + LazyTest, + // ignore: invalid_use_of_protected_member + dartComponentVersion: ReactDartComponentVersion.component2, + renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child), + ); + }); + }); +} diff --git a/test/react_lazy_test.html b/test/react_lazy_test.html new file mode 100644 index 00000000..73faf0d6 --- /dev/null +++ b/test/react_lazy_test.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/test/react_suspense_test.dart b/test/react_suspense_test.dart index 50ad0c22..b2f7c4bb 100644 --- a/test/react_suspense_test.dart +++ b/test/react_suspense_test.dart @@ -3,39 +3,15 @@ library react_test_utils_test; import 'dart:html'; -import 'dart:js_util'; import 'package:js/js.dart'; import 'package:react/react.dart' as react; -import 'package:react/react.dart'; -import 'package:react/react_client/component_factory.dart'; import 'package:react/react_client/react_interop.dart'; import 'package:react/react_dom.dart' as react_dom; -import 'package:react/src/js_interop_util.dart'; import 'package:test/test.dart'; import './react_suspense_lazy_component.dart' deferred as simple; -@JS('React.lazy') -external ReactClass jsLazy(Promise Function() factory); - -// Only intended for testing purposes, Please do not copy/paste this into repo. -// This will most likely be added to the PUBLIC api in the future, -// but needs more testing and Typing decisions to be made first. -ReactJsComponentFactoryProxy lazy(Future Function() factory) => - ReactJsComponentFactoryProxy( - jsLazy( - allowInterop( - () => futureToPromise( - // React.lazy only supports "default exports" from a module. - // This `{default: yourExport}` workaround can be found in the React.lazy RFC comments. - // See: https://github.com/reactjs/rfcs/pull/64#issuecomment-431507924 - (() async => jsify({'default': (await factory()).type}))(), - ), - ), - ), - ); - main() { group('Suspense', () { test('renders fallback UI first followed by the real component', () async { From 1b1ad625f23b4ef6c6b16f7a99da230628289c12 Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 9 Sep 2024 17:14:11 -0700 Subject: [PATCH 02/14] format --- lib/react_client/react_interop.dart | 1 - test/factory/common_factory_tests.dart | 4 +++- test/react_lazy_test.dart | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index c68dc46e..e38d506c 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -277,7 +277,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 loaded only when they are rendered for the first time, which can help improve the initial loading time of your application. diff --git a/test/factory/common_factory_tests.dart b/test/factory/common_factory_tests.dart index 66ee402e..5ac6729e 100644 --- a/test/factory/common_factory_tests.dart +++ b/test/factory/common_factory_tests.dart @@ -26,7 +26,9 @@ import '../util.dart'; /// [dartComponentVersion] should be specified for all components with Dart render code in order to /// properly test `props.children`, forwardRef compatibility, etc. void commonFactoryTests(ReactComponentFactoryProxy factory, - {String? dartComponentVersion, bool skipPropValuesTest = false, ReactElement Function(dynamic children)? renderWrapper}) { + {String? dartComponentVersion, + bool skipPropValuesTest = false, + ReactElement Function(dynamic children)? renderWrapper}) { _childKeyWarningTests( factory, renderWithUniqueOwnerName: (ReactElement Function() render) => _renderWithUniqueOwnerName(render, renderWrapper), diff --git a/test/react_lazy_test.dart b/test/react_lazy_test.dart index c903a8b8..bffeffdb 100644 --- a/test/react_lazy_test.dart +++ b/test/react_lazy_test.dart @@ -11,9 +11,9 @@ main() { group('lazy', () { group('- common factory behavior -', () { final LazyTest = react.lazy(() async => react.registerFunctionComponent((props) { - props['onDartRender']?.call(props); - return react.div({...props}); - })); + props['onDartRender']?.call(props); + return react.div({...props}); + })); commonFactoryTests( LazyTest, From 390a5b6cf13cbccacb5aa0e94d327c352c98f3a2 Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Fri, 13 Sep 2024 08:57:25 -0700 Subject: [PATCH 03/14] format --- lib/react_client/react_interop.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index e38d506c..91bd343e 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -294,7 +294,7 @@ ReactComponentFactoryProxy memo2(ReactComponentFactoryProxy factory, /// { /// fallback: 'Loading...', /// }, -/// lazyComponent(), +/// lazyComponent({}), /// ); /// ``` /// @@ -304,10 +304,17 @@ ReactComponentFactoryProxy memo2(ReactComponentFactoryProxy factory, /// 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() loadFunction) { - final hoc = React.lazy(allowInterop(() => futureToPromise((() async { - final factory = await loadFunction(); - return jsify({'default': factory.type}); - })()))); + final hoc = React.lazy( + allowInterop( + () => futureToPromise( + (() async { + final factory = await loadFunction(); + return jsify({'default': factory.type}); + })(), + ), + ), + ); + setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2); return ReactDartWrappedComponentFactoryProxy(hoc); From 49faf3fe19e908fe43cfc6c44486f182df18e328 Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 16 Sep 2024 14:44:44 -0700 Subject: [PATCH 04/14] cleanup lazy doc comment --- lib/react_client/react_interop.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index 91bd343e..b1b58fc8 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -279,15 +279,19 @@ ReactComponentFactoryProxy memo2(ReactComponentFactoryProxy factory, /// 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 loaded only when they are rendered for the first time, which can help improve the initial loading time of your application. +/// 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` widget. The `Suspense` widget allows you to specify what should be displayed while the lazy component is loading, such as a loading spinner or a placeholder. +/// 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_client.dart' show lazy, Suspense; +/// import 'package:react/react.dart' show lazy, Suspense; +/// import './simple_component.dart' deferred as simple; /// -/// final lazyComponent = lazy(() => import('./path/to/lazy_component.dart')); +/// final lazyComponent = lazy(() async { +/// await simple.loadLibrary(); +/// return simple.SimpleComponent; +/// }); /// /// // Wrap the lazy component with Suspense /// final app = Suspense( @@ -298,7 +302,6 @@ ReactComponentFactoryProxy memo2(ReactComponentFactoryProxy factory, /// ); /// ``` /// -/// Note: The `lazy` function is part of the `react_client` library in React Dart. /// Defer loading a component’s code until it is rendered for the first time. /// /// Lazy components need to be wrapped with `Suspense` to render. From e7741ae29e4440be98b735b488c62a13422ccffb Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Wed, 18 Sep 2024 09:33:55 -0700 Subject: [PATCH 05/14] align parameter names to react docs --- lib/react_client/react_interop.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index b1b58fc8..661618b6 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -44,7 +44,7 @@ abstract class React { dynamic wrapperFunction, [ bool Function(JsMap prevProps, JsMap nextProps)? areEqual, ]); - external static ReactClass lazy(Promise Function() loadFunction); + external static ReactClass lazy(Promise Function() load); external static bool isValidElement(dynamic object); @@ -306,12 +306,12 @@ ReactComponentFactoryProxy memo2(ReactComponentFactoryProxy factory, /// /// 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() loadFunction) { +ReactComponentFactoryProxy lazy(Future Function() load) { final hoc = React.lazy( allowInterop( () => futureToPromise( (() async { - final factory = await loadFunction(); + final factory = await load(); return jsify({'default': factory.type}); })(), ), From 11fbf7d89af4c5c173ac54a54f6b38125e12c537 Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Fri, 27 Sep 2024 15:11:29 -0700 Subject: [PATCH 06/14] Fix dummyEvent setup that never should have worked --- test/factory/common_factory_tests.dart | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/test/factory/common_factory_tests.dart b/test/factory/common_factory_tests.dart index 5ac6729e..91767d53 100644 --- a/test/factory/common_factory_tests.dart +++ b/test/factory/common_factory_tests.dart @@ -277,15 +277,22 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) { reason: 'test setup: component must pass props into props.onDartRender'); }); - late react.SyntheticMouseEvent event; - final divRef = react.createRef(); - render(react.div({ - 'ref': divRef, - 'onClick': (react.SyntheticMouseEvent e) => event = e, - })); - rtu.Simulate.click(divRef); + late react.SyntheticMouseEvent dummyEvent; + setUpAll(() { + final mountNode = DivElement(); + document.body!.append(mountNode); + addTearDown(() { + react_dom.unmountComponentAtNode(mountNode); + mountNode.remove(); + }); - final dummyEvent = event; + final divRef = react.createRef(); + react_dom.render(react.div({ + 'ref': divRef, + 'onClick': (react.SyntheticMouseEvent e) => dummyEvent = e, + }), mountNode); + divRef.current!.click(); + }); for (final eventCase in eventCases.where((helper) => helper.isDart)) { test(eventCase.description, () { From c80f88fb6dee7cb2004b2dca94b0736cb10a4e3e Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Fri, 27 Sep 2024 15:24:29 -0700 Subject: [PATCH 07/14] Add lazy test coverage, add wrapper in react-dart --- lib/react_client/react_interop.dart | 17 ++++++- test/factory/common_factory_tests.dart | 11 ++-- test/react_lazy_test.dart | 69 +++++++++++++++++++++++--- test/react_lazy_test.html | 1 + 4 files changed, 84 insertions(+), 14 deletions(-) diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index 661618b6..764f8db2 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -312,14 +312,27 @@ ReactComponentFactoryProxy lazy(Future Function() lo () => futureToPromise( (() async { final factory = await load(); - return jsify({'default': factory.type}); + // 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); } diff --git a/test/factory/common_factory_tests.dart b/test/factory/common_factory_tests.dart index 91767d53..c2fc2edf 100644 --- a/test/factory/common_factory_tests.dart +++ b/test/factory/common_factory_tests.dart @@ -28,6 +28,7 @@ import '../util.dart'; void commonFactoryTests(ReactComponentFactoryProxy factory, {String? dartComponentVersion, bool skipPropValuesTest = false, + bool isNonDartComponentWithDartWrapper = false, ReactElement Function(dynamic children)? renderWrapper}) { _childKeyWarningTests( factory, @@ -115,7 +116,7 @@ void commonFactoryTests(ReactComponentFactoryProxy factory, shouldAlwaysBeList: isDartComponent2(factory({}))); }); - if (isDartComponent(factory({}))) { + if (isDartComponent(factory({})) && !isNonDartComponentWithDartWrapper) { group('passes children to the Dart component when specified as', () { final notCalledSentinelValue = Object(); dynamic childrenFromLastRender; @@ -173,7 +174,7 @@ void commonFactoryTests(ReactComponentFactoryProxy factory, } } - if (isDartComponent2(factory({}))) { + if (isDartComponent2(factory({})) && !isNonDartComponentWithDartWrapper) { test('executes Dart render code in the component zone', () { final oldComponentZone = componentZone; addTearDown(() => componentZone = oldComponentZone); @@ -193,7 +194,9 @@ void commonFactoryTests(ReactComponentFactoryProxy factory, } } -void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) { +void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory, { + bool isNonDartComponentWithDartWrapper = false, +}) { Element renderAndGetRootNode(ReactElement content) { final mountNode = Element.div(); react_dom.render(content, mountNode); @@ -270,7 +273,7 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) { } }); - if (isDartComponent(factory({}))) { + if (isDartComponent(factory({})) && !isNonDartComponentWithDartWrapper) { group('in a way that the handlers are callable from within the Dart component:', () { setUpAll(() { expect(propsFromDartRender, isNotNull, diff --git a/test/react_lazy_test.dart b/test/react_lazy_test.dart index bffeffdb..2d87e3dd 100644 --- a/test/react_lazy_test.dart +++ b/test/react_lazy_test.dart @@ -1,7 +1,13 @@ @TestOn('browser') +@JS() library react.react_lazy_test; +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_client/component_factory.dart'; import 'package:react/react_client/react_interop.dart'; import 'package:test/test.dart'; @@ -9,18 +15,65 @@ import 'factory/common_factory_tests.dart'; main() { group('lazy', () { - group('- common factory behavior -', () { - final LazyTest = react.lazy(() async => react.registerFunctionComponent((props) { + group('Dart component', () { + final LazyTest = react.lazy(() async => react.forwardRef2((props, ref) { + useImperativeHandle(ref, () => TestImperativeHandle()); props['onDartRender']?.call(props); return react.div({...props}); })); - commonFactoryTests( - LazyTest, - // ignore: invalid_use_of_protected_member - dartComponentVersion: ReactDartComponentVersion.component2, - renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child), - ); + group('- common factory behavior -', () { + commonFactoryTests( + LazyTest, + // ignore: invalid_use_of_protected_member + dartComponentVersion: ReactDartComponentVersion.component2, + renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child), + ); + }); + + group('- dom event handler wrapping -', () { + domEventHandlerWrappingTests(LazyTest); + }); + + group('- refs -', () { + refTests(LazyTest, verifyRefValue: (ref) { + expect(ref, isA()); + }); + }); + }); + + group('JS component', () { + final LazyJsTest = react.lazy(() async => ReactJsComponentFactoryProxy(_JsFoo)); + + group('- common factory behavior -', () { + commonFactoryTests( + LazyJsTest, + // ignore: invalid_use_of_protected_member + dartComponentVersion: ReactDartComponentVersion.component2, + // This isn't a Dart component, but it's detected as one by tests due to the factory's dartComponentVersion + isNonDartComponentWithDartWrapper: true, + renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child), + ); + }); + + group('- dom event handler wrapping -', () { + domEventHandlerWrappingTests( + LazyJsTest, + // This isn't a Dart component, but it's detected as one by tests due to the factory's dartComponentVersion + isNonDartComponentWithDartWrapper: true, + ); + }); + + group('- refs -', () { + refTests(LazyJsTest, verifyRefValue: (ref) { + expect(getProperty(ref as Object, 'constructor'), same(_JsFoo)); + }); + }); }); }); } + +class TestImperativeHandle {} + +@JS() +external ReactClass get _JsFoo; diff --git a/test/react_lazy_test.html b/test/react_lazy_test.html index 73faf0d6..2f98b084 100644 --- a/test/react_lazy_test.html +++ b/test/react_lazy_test.html @@ -7,6 +7,7 @@ + From 94aa9f3a372cc5594bfb3759afaf2fa67b63c07d Mon Sep 17 00:00:00 2001 From: Greg Littlefield Date: Fri, 27 Sep 2024 16:10:37 -0700 Subject: [PATCH 08/14] Format --- test/factory/common_factory_tests.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/factory/common_factory_tests.dart b/test/factory/common_factory_tests.dart index c2fc2edf..d0d800ba 100644 --- a/test/factory/common_factory_tests.dart +++ b/test/factory/common_factory_tests.dart @@ -194,7 +194,8 @@ void commonFactoryTests(ReactComponentFactoryProxy factory, } } -void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory, { +void domEventHandlerWrappingTests( + ReactComponentFactoryProxy factory, { bool isNonDartComponentWithDartWrapper = false, }) { Element renderAndGetRootNode(ReactElement content) { @@ -290,10 +291,12 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory, { }); final divRef = react.createRef(); - react_dom.render(react.div({ - 'ref': divRef, - 'onClick': (react.SyntheticMouseEvent e) => dummyEvent = e, - }), mountNode); + react_dom.render( + react.div({ + 'ref': divRef, + 'onClick': (react.SyntheticMouseEvent e) => dummyEvent = e, + }), + mountNode); divRef.current!.click(); }); From da7b4129dde7e5dc841a289f3e79bd21e3947bba Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 30 Sep 2024 13:54:04 -0700 Subject: [PATCH 09/14] 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; From 2aea0690f9762597f2cde81a9f378c57fa09115d Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 30 Sep 2024 13:54:17 -0700 Subject: [PATCH 10/14] format --- lib/src/react_client/lazy.dart | 1 - test/react_lazy_test.dart | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/src/react_client/lazy.dart b/lib/src/react_client/lazy.dart index c79bbaa5..50972568 100644 --- a/lib/src/react_client/lazy.dart +++ b/lib/src/react_client/lazy.dart @@ -1,4 +1,3 @@ - import 'dart:js'; import 'dart:js_util'; diff --git a/test/react_lazy_test.dart b/test/react_lazy_test.dart index 635d1d2b..6fc9fa16 100644 --- a/test/react_lazy_test.dart +++ b/test/react_lazy_test.dart @@ -23,12 +23,18 @@ main() { const errorString = 'intentional future error'; final errors = []; final errorCompleter = Completer(); - final ThrowingLazyTest = react.lazy(() async { throw Exception(errorString);}); + 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); + + 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))); From b552b7d8d42c03d12d6e3669af9f11ef58fab53d Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 30 Sep 2024 14:19:48 -0700 Subject: [PATCH 11/14] use Future.sync JIC --- lib/src/react_client/lazy.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/react_client/lazy.dart b/lib/src/react_client/lazy.dart index 50972568..6dae4ccf 100644 --- a/lib/src/react_client/lazy.dart +++ b/lib/src/react_client/lazy.dart @@ -39,7 +39,7 @@ ReactComponentFactoryProxy lazy(Future Function() lo final hoc = React.lazy( allowInterop( () => futureToPromise( - (() async { + Future.sync(() 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 @@ -54,7 +54,7 @@ ReactComponentFactoryProxy lazy(Future Function() lo ); }); return jsify({'default': wrapper.type}); - })(), + }), ), ), ); From f0c45486c55be4f6044a8124e7a7c7431331d102 Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 7 Oct 2024 09:45:08 -0700 Subject: [PATCH 12/14] Add Lazy HOC name wrapping --- lib/src/react_client/lazy.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/react_client/lazy.dart b/lib/src/react_client/lazy.dart index 6dae4ccf..b1958f09 100644 --- a/lib/src/react_client/lazy.dart +++ b/lib/src/react_client/lazy.dart @@ -52,7 +52,7 @@ ReactComponentFactoryProxy lazy(Future Function() lo if (children != null && !(children is List && children.isEmpty)) children, ], ); - }); + }, displayName: 'Lazy(${getProperty(factory.type as Object, 'name') ?? 'Anonymous'})'); return jsify({'default': wrapper.type}); }), ), From 471014070b5c30b4dba289c96fb524bfe71e0861 Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 7 Oct 2024 14:01:11 -0700 Subject: [PATCH 13/14] Apply greg's getComponentName --- lib/src/react_client/lazy.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/src/react_client/lazy.dart b/lib/src/react_client/lazy.dart index b1958f09..c2c2a972 100644 --- a/lib/src/react_client/lazy.dart +++ b/lib/src/react_client/lazy.dart @@ -52,7 +52,7 @@ ReactComponentFactoryProxy lazy(Future Function() lo if (children != null && !(children is List && children.isEmpty)) children, ], ); - }, displayName: 'Lazy(${getProperty(factory.type as Object, 'name') ?? 'Anonymous'})'); + }, displayName: 'Lazy(${_getComponentName(factory.type) ?? 'Anonymous'})'); return jsify({'default': wrapper.type}); }), ), @@ -65,3 +65,17 @@ ReactComponentFactoryProxy lazy(Future Function() lo setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2); return ReactDartWrappedComponentFactoryProxy(hoc); } + +String? _getComponentName(Object? type) { + if (type == null) return null; + + if (type is String) return type; + + final name = getProperty(type, 'name'); + if (name is String) return name; + + final displayName = getProperty(type, 'displayName'); + if (displayName is String) return displayName; + + return null; +} From 2ac0cbc0868f66b5c0d0b238c411fad9263d6d43 Mon Sep 17 00:00:00 2001 From: Keal Jones Date: Mon, 7 Oct 2024 14:05:52 -0700 Subject: [PATCH 14/14] Name lazy wrapper component --- lib/src/react_client/lazy.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/react_client/lazy.dart b/lib/src/react_client/lazy.dart index c2c2a972..65ba9e5a 100644 --- a/lib/src/react_client/lazy.dart +++ b/lib/src/react_client/lazy.dart @@ -52,7 +52,7 @@ ReactComponentFactoryProxy lazy(Future Function() lo if (children != null && !(children is List && children.isEmpty)) children, ], ); - }, displayName: 'Lazy(${_getComponentName(factory.type) ?? 'Anonymous'})'); + }, displayName: 'LazyWrapper(${_getComponentName(factory.type) ?? 'Anonymous'})'); return jsify({'default': wrapper.type}); }), ),