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
40 changes: 12 additions & 28 deletions example/suspense/suspense.dart
Original file line number Diff line number Diff line change
@@ -1,55 +1,39 @@
@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();
final lazyComponent = react.lazy(() async {
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)
]);
}
1 change: 1 addition & 0 deletions lib/react.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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/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;

Expand Down
2 changes: 2 additions & 0 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +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 'package:react/src/js_interop_util.dart';

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

Expand All @@ -42,6 +43,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
81 changes: 81 additions & 0 deletions lib/src/react_client/lazy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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<ReactComponentFactoryProxy> Function() load) {
final hoc = React.lazy(
allowInterop(
() => futureToPromise(
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
// 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,
],
);
}, displayName: 'LazyWrapper(${_getComponentName(factory.type) ?? 'Anonymous'})');
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);
}

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;
}
49 changes: 32 additions & 17 deletions test/factory/common_factory_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ 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,
bool isNonDartComponentWithDartWrapper = 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 @@ -113,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;
Expand Down Expand Up @@ -171,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);
Expand All @@ -191,7 +194,10 @@ 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);
Expand Down Expand Up @@ -268,22 +274,31 @@ 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,
reason: 'test setup: component must pass props into props.onDartRender');
});

late react.SyntheticMouseEvent event;
final divRef = react.createRef<DivElement>();
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<DivElement>();
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, () {
Expand Down Expand Up @@ -532,7 +547,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 +592,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
Loading
Loading