diff --git a/example/suspense/index.html b/example/suspense/index.html
index d37af6770..cee072da8 100644
--- a/example/suspense/index.html
+++ b/example/suspense/index.html
@@ -16,23 +16,56 @@
-
-
-
- over_react Suspense example
-
-
-
+
+
+
+ over_react Suspense example
+
+
+
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/example/suspense/lazy.dart b/example/suspense/lazy.dart
deleted file mode 100644
index 3b33d36f3..000000000
--- a/example/suspense/lazy.dart
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2023 Workiva Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import 'dart:js_util';
-
-import 'package:js/js.dart';
-import 'package:over_react/over_react.dart';
-import 'package:over_react/src/util/promise_interop.dart';
-import 'package:react/react_client/react_interop.dart' as react_interop;
-import 'package:over_react/js_component.dart';
-import 'package:react/react_client/component_factory.dart';
-
-@JS('React.lazy')
-external react_interop.ReactClass jsLazy(Promise Function() factory);
-
-// Only intended for testing purposes, Please do not copy/paste this into your 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.
-UiFactory lazy(Future> Function() factory, UiFactoryConfig factoryConfig) {
- return uiJsComponent(
- 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 {
- //resolvedFactory = await factory();
- return jsify({'default': (await factory()).elementType});
- })(),
- ),
- ),
- ),
- ),
- factoryConfig,
- );
-}
diff --git a/example/suspense/main.dart b/example/suspense/main.dart
index a7c76e69f..3a2b07093 100644
--- a/example/suspense/main.dart
+++ b/example/suspense/main.dart
@@ -17,18 +17,17 @@ import 'dart:html';
import 'package:over_react/over_react.dart';
import 'package:react/react_dom.dart' as react_dom;
import 'counter_component.dart' deferred as lazy_component;
-import 'lazy.dart';
import 'third_party_file.dart';
-final LazyCounter = lazy(() async {
- await lazy_component.loadLibrary();
+UiFactory LazyCounter = lazy(() async {
await Future.delayed(Duration(seconds: 5));
+ await lazy_component.loadLibrary();
return lazy_component.Counter;
},
- UiFactoryConfig(
- propsFactory: PropsFactory.fromUiFactory(CounterPropsMapView),
- displayName: 'This does nothing...',
- ));
+UiFactoryConfig(
+ propsFactory: PropsFactory.fromUiFactory(CounterPropsMapView)
+ )
+);
void main() {
react_dom.render(
@@ -38,7 +37,7 @@ void main() {
'I am a fallback UI that will show while we load the lazy component! The load time is artificially inflated to last an additional 5 seconds just to prove it\'s working!',
)
)(
- (LazyCounter()..initialCount = 2)(
+ (LazyCounter())(
(Dom.div()..id = 'Heyyy!')(),
),
),
diff --git a/lib/over_react.dart b/lib/over_react.dart
index e4f6a35fd..c495700ee 100644
--- a/lib/over_react.dart
+++ b/lib/over_react.dart
@@ -101,6 +101,7 @@ export 'src/util/guid_util.dart';
export 'src/util/hoc.dart';
export 'src/util/handler_chain_util.dart';
export 'src/util/key_constants.dart';
+export 'src/util/lazy.dart';
export 'src/util/map_util.dart';
export 'src/util/memo.dart';
export 'src/util/pretty_print.dart';
diff --git a/lib/src/util/lazy.dart b/lib/src/util/lazy.dart
new file mode 100644
index 000000000..46b522c62
--- /dev/null
+++ b/lib/src/util/lazy.dart
@@ -0,0 +1,111 @@
+// Copyright 2020 Workiva Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+library over_react.lazy;
+
+import 'package:over_react/over_react.dart';
+import 'package:react/react.dart' as react;
+
+import '../component_declaration/function_component.dart';
+
+/// A HOC that creates a "lazy" component that lets you defer loading a component’s code until it is rendered for the first time.
+///
+/// Returns a `UiFactory` you can use just render in your tree. While the code for the lazy component is still loading,
+/// attempting to render it will suspend. Use to display a loading indicator while it’s loading.
+/// [load] is a function that should return a `Future>` that resolves to the component to be rendered.
+/// [_config] should be a `UiFactoryConfig` or `null` and is only `dynamic` to avoid an unnecessary cast in the boilerplate.
+///
+/// React will not call [load] until the first time the component is rendered.
+/// After React first calls [load], it will wait for it to resolve, and then render the resolved value.
+/// Both the returned Future and the Future's resolved value will be cached, so React will not call [load] more than once.
+/// If the Future rejects, React will throw the rejection reason for the nearest Error Boundary to handle.
+///
+/// Example:
+/// ```dart
+/// import 'package:over_react/over_react.dart';
+///
+/// part 'main.over_react.g.dart';
+///
+/// mixin ALazyComponentPropsMixin on UiProps {}
+///
+/// UiFactory ALazyComponent = lazy(
+/// () async {
+/// final componentModule = await loadComponent();
+/// return uiForwardRef(
+/// (props, ref) {
+/// return (componentModule.AnotherComponent()
+/// ..ref = ref
+/// ..addProps(props)
+/// )(props.children);
+/// },
+/// _$ALazyComponentConfig,
+/// );
+/// },
+/// _$ALazyComponentConfig
+/// );
+/// ```
+///
+/// > __NOTE:__ A lazy component MUST be wrapped with a `Suspense` component to provide a fallback ui while it loads.
+///
+/// ```dart
+/// (Suspense()
+/// ..fallback = Dom.p()('Loading...')
+/// )(
+/// ALazyComponent()(),
+/// );
+/// ```
+/// See: .
+UiFactory lazy(
+ Future> Function() load, /* UiFactoryConfig */ dynamic _config) {
+ ArgumentError.checkNotNull(_config, '_config');
+ if (_config is! UiFactoryConfig) {
+ throw ArgumentError('_config is required when using a custom props class and should be a UiFactoryConfig. Make sure you are '
+ r'using either the generated factory config (i.e. _$FooConfig) or manually declaring your config correctly.');
+ }
+ // ignore: invalid_use_of_protected_member
+ var propsFactory = _config.propsFactory;
+
+ final lazyFactoryProxy = react.lazy(() async {
+ final factory = await load();
+ return factory().componentFactory!;
+ });
+
+ if (propsFactory == null) {
+ if (TProps != UiProps && TProps != GenericUiProps) {
+ throw ArgumentError(
+ 'config.propsFactory must be provided when using custom props classes');
+ }
+ propsFactory = PropsFactory.fromUiFactory(
+ ([backingMap]) => GenericUiProps(lazyFactoryProxy, backingMap))
+ as PropsFactory;
+ }
+ // Work around propsFactory not getting promoted to non-nullable in _uiFactory: https://github.com/dart-lang/language/issues/1536
+ final nonNullablePropsFactory = propsFactory;
+
+ TProps _uiFactory([Map? props]) {
+ TProps builder;
+ if (props == null) {
+ // propsFactory should get promoted to non-nullable here, but it does not some reason propsF
+ builder = nonNullablePropsFactory.jsMap(JsBackedMap());
+ } else if (props is JsBackedMap) {
+ builder = nonNullablePropsFactory.jsMap(props);
+ } else {
+ builder = nonNullablePropsFactory.map(props);
+ }
+
+ return builder..componentFactory = lazyFactoryProxy;
+ }
+
+ return _uiFactory;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index fc56d6216..6c4002692 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,7 +15,7 @@ dependencies:
meta: ^1.6.0
package_config: ^2.1.0
path: ^1.5.1
- react: ^7.1.0
+ react: ^7.2.0
redux: ^5.0.0
source_span: ^1.4.1
transformer_utils: ^0.2.6
diff --git a/snippets/snippets.json b/snippets/snippets.json
index 46c4c78d8..4baaabf9f 100644
--- a/snippets/snippets.json
+++ b/snippets/snippets.json
@@ -1,4 +1,99 @@
{
+ "lazyFunctionComponent": {
+ "prefix": "orLzyFunc",
+ "body": [
+ "import 'package:over_react/over_react.dart';",
+ "",
+ "part '${TM_FILENAME_BASE}.over_react.g.dart';",
+ "",
+ "mixin ${1:MyComponent}PropsMixin on UiProps {}",
+ "",
+ "UiFactory<${1:MyComponent}PropsMixin> ${1:MyComponent} = lazy(",
+ "\t() async {",
+ "\t\t// Await required futures here.",
+ "\t\treturn uiFunction(",
+ "\t\t\t(props) {},",
+ "\t\t\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
+ "\t\t);",
+ "\t},",
+ "\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
+ ");"
+ ],
+ "description": "Creates an OverReact lazy wrapped uiFunction component with an abbreviated props declaration"
+ },
+ "lazyForwardRefComponent": {
+ "prefix": "orLzyFwdRef",
+ "body": [
+ "import 'package:over_react/over_react.dart';",
+ "",
+ "part '${TM_FILENAME_BASE}.over_react.g.dart';",
+ "",
+ "mixin ${1:MyComponent}PropsMixin on UiProps {}",
+ "",
+ "/// Must be wrapped in a `Suspense` component to display a fallback ui while loading.",
+ "UiFactory<${1:MyComponent}PropsMixin> ${1:MyComponent} = lazy(",
+ "\t() async {",
+ "\t\t// Await required futures here.",
+ "\t\treturn uiForwardRef(",
+ "\t\t\t(props, ref) {},",
+ "\t\t\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
+ "\t\t);",
+ "\t},",
+ "\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
+ ");"
+ ],
+ "description": "Creates an OverReact lazy wrapped uiForwardRef component with an abbreviated props declaration"
+ },
+ "forwardRefComponent": {
+ "prefix": "orFwdRef",
+ "body": [
+ "import 'package:over_react/over_react.dart';",
+ "",
+ "part '${TM_FILENAME_BASE}.over_react.g.dart';",
+ "",
+ "mixin ${1:MyComponent}Props on UiProps {}",
+ "",
+ "UiFactory<${1:MyComponent}Props> ${1:MyComponent} = uiForwardRef(",
+ "\t(props, ref) {},",
+ "\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
+ ");"
+ ],
+ "description": "Creates an OverReact forwardRef component with an abbreviated props declaration"
+ },
+ "abbreviatedFunctionComponent": {
+ "prefix": "orFunc",
+ "body": [
+ "import 'package:over_react/over_react.dart';",
+ "",
+ "part '${TM_FILENAME_BASE}.over_react.g.dart';",
+ "",
+ "UiFactory<${1:MyComponent}Props> ${1:MyComponent} = uiFunction(",
+ "\t(props) {},",
+ "\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
+ ");",
+ "",
+ "mixin ${1:MyComponent}Props on UiProps {}"
+ ],
+ "description": "Creates an OverReact function component with an abbreviated props declaration"
+ },
+ "functionComponent": {
+ "prefix": "orAdvFunc",
+ "body": [
+ "import 'package:over_react/over_react.dart';",
+ "",
+ "part '${TM_FILENAME_BASE}.over_react.g.dart';",
+ "",
+ "mixin ${1:MyComponent}PropsMixin on UiProps {}",
+ "",
+ "class ${1:MyComponent}Props = UiProps with ${1:MyComponent}PropsMixin;",
+ "",
+ "UiFactory<${1:MyComponent}Props> ${1:MyComponent} = uiFunction(",
+ "\t(props) {},",
+ "\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
+ ");"
+ ],
+ "description": "Creates an OverReact function component with a props class alias"
+ },
"statelessComponent": {
"prefix": "orAdvStless",
"body": [
@@ -127,40 +222,5 @@
"}"
],
"description": "Creates a stateless and connected OverReact component"
- },
- "abbreviatedFunctionComponent": {
- "prefix": "orFunc",
- "body": [
- "import 'package:over_react/over_react.dart';",
- "",
- "part '${TM_FILENAME_BASE}.over_react.g.dart';",
- "",
- "UiFactory<${1:MyComponent}Props> ${1:MyComponent} = uiFunction(",
- "\t(props) {},",
- "\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
- ");",
- "",
- "mixin ${1:MyComponent}Props on UiProps {}"
- ],
- "description": "Creates an OverReact function component with an abbreviated props declaration"
- },
- "functionComponent": {
- "prefix": "orAdvFunc",
- "body": [
- "import 'package:over_react/over_react.dart';",
- "",
- "part '${TM_FILENAME_BASE}.over_react.g.dart';",
- "",
- "UiFactory<${1:MyComponent}Props> ${1:MyComponent} = uiFunction(",
- "\t(props) {},",
- "\t_$${1:MyComponent}Config, // ignore: undefined_identifier",
- ");",
- "",
- "mixin ${1:MyComponent}PropsMixin on UiProps {}",
- "",
- "class ${1:MyComponent}Props = UiProps with ${1:MyComponent}PropsMixin;",
- ""
- ],
- "description": "Creates an OverReact function component with a props class alias"
}
}
diff --git a/snippets/snippets.xml b/snippets/snippets.xml
index d040b67dc..eb4970a5c 100644
--- a/snippets/snippets.xml
+++ b/snippets/snippets.xml
@@ -1,3 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/over_react/component/lazy_test.dart b/test/over_react/component/lazy_test.dart
new file mode 100644
index 000000000..46954047f
--- /dev/null
+++ b/test/over_react/component/lazy_test.dart
@@ -0,0 +1,719 @@
+@TestOn('browser')
+@JS()
+library rmui.test.unit.component.lazy_test;
+
+import 'dart:convert';
+import 'dart:html';
+
+import 'package:js/js.dart';
+import 'package:over_react/js_component.dart';
+import 'package:over_react/over_react.dart';
+import 'package:react/react_client.dart';
+import 'package:react/react_client/react_interop.dart' hide createRef;
+import 'package:react_testing_library/matchers.dart';
+import 'package:react_testing_library/react_testing_library.dart';
+
+import 'package:test/test.dart';
+import 'package:over_react/components.dart' as components;
+
+import '../component_declaration/builder_integration_tests/new_boilerplate/function_component_test.dart';
+import '../util/prop_conversion_test.dart';
+import '../util/ref_test_cases.dart';
+
+part 'lazy_test.over_react.g.dart';
+
+main() {
+ enableTestMode();
+
+ group('lazy', () {
+ group('config argument:', () {
+ group('generated:', () {
+ group('initializes the factory variable with a function', () {
+ test('that returns a new props class implementation instance', () {
+ final instance = lazy(() async => TestDart, _$TestDartConfig)();
+ expect(instance, isA());
+ expect(instance, isA