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 @@ + +