Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FED-3207 Add react lazy #406

Merged
merged 15 commits into from
Oct 7, 2024
Merged
37 changes: 11 additions & 26 deletions example/suspense/suspense.dart
Original file line number Diff line number Diff line change
@@ -1,55 +1,40 @@
@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<ReactComponentFactoryProxy> 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({});

react_dom.render(content, querySelector('#content'));
}

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)
]);
}
2 changes: 1 addition & 1 deletion lib/react.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
49 changes: 49 additions & 0 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved

typedef ReactJsComponentFactory = ReactElement Function(dynamic props, dynamic children);

// ----------------------------------------------------------------------------
Expand All @@ -42,6 +44,7 @@ abstract class React {
dynamic wrapperFunction, [
bool Function(JsMap prevProps, JsMap nextProps)? areEqual,
]);
external static ReactClass lazy(Promise Function() load);

external static bool isValidElement(dynamic object);

Expand Down Expand Up @@ -274,6 +277,52 @@ 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<ReactComponentFactoryProxy> Function() load) {
greglittlefield-wf marked this conversation as resolved.
Show resolved Hide resolved
final hoc = React.lazy(
allowInterop(
() => futureToPromise(
(() async {
final factory = await load();
return jsify({'default': factory.type});
})(),
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved
),
),
);

setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2);
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved

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);
Expand Down
12 changes: 7 additions & 5 deletions test/factory/common_factory_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ 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`', () {
Expand Down Expand Up @@ -532,7 +534,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.'));
Expand Down Expand Up @@ -577,12 +579,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 {
Expand Down
26 changes: 26 additions & 0 deletions test/react_lazy_test.dart
Original file line number Diff line number Diff line change
@@ -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', () {
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved
group('- common factory behavior -', () {
final LazyTest = react.lazy(() async => react.registerFunctionComponent((props) {
props['onDartRender']?.call(props);
return react.div({...props});
}));
kealjones-wk marked this conversation as resolved.
Show resolved Hide resolved

commonFactoryTests(
LazyTest,
// ignore: invalid_use_of_protected_member
dartComponentVersion: ReactDartComponentVersion.component2,
renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child),
);
});
});
}
12 changes: 12 additions & 0 deletions test/react_lazy_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title></title>
<script src="packages/react/react_with_addons.js"></script>
<script src="packages/react/react_dom.js"></script>
<link rel="x-dart-test" href="react_lazy_test.dart" />
<script src="packages/test/dart.js"></script>
</head>
<body></body>
</html>
24 changes: 0 additions & 24 deletions test/react_suspense_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReactComponentFactoryProxy> 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 {
Expand Down
Loading