Skip to content

Commit 9a9899f

Browse files
Merge pull request #406 from Workiva/FED-3114-lazy
FED-3207 Add react lazy
2 parents c0da27b + 2ac0cbc commit 9a9899f

File tree

8 files changed

+268
-72
lines changed

8 files changed

+268
-72
lines changed

example/suspense/suspense.dart

+12-28
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,39 @@
11
@JS()
2-
library js_components;
2+
library example.suspense.suspense;
33

44
import 'dart:html';
5-
import 'dart:js_util';
65

76
import 'package:js/js.dart';
7+
import 'package:react/hooks.dart';
88
import 'package:react/react.dart' as react;
9-
import 'package:react/react_client.dart';
10-
import 'package:react/react_client/react_interop.dart';
119
import 'package:react/react_dom.dart' as react_dom;
12-
import 'package:react/src/js_interop_util.dart';
1310
import './simple_component.dart' deferred as simple;
1411

15-
@JS('React.lazy')
16-
external ReactClass jsLazy(Promise Function() factory);
17-
18-
// Only intended for testing purposes, Please do not copy/paste this into repo.
19-
// This will most likely be added to the PUBLIC api in the future,
20-
// but needs more testing and Typing decisions to be made first.
21-
ReactJsComponentFactoryProxy lazy(Future<ReactComponentFactoryProxy> Function() factory) =>
22-
ReactJsComponentFactoryProxy(
23-
jsLazy(
24-
allowInterop(
25-
() => futureToPromise(
26-
// React.lazy only supports "default exports" from a module.
27-
// This `{default: yourExport}` workaround can be found in the React.lazy RFC comments.
28-
// See: https://github.com/reactjs/rfcs/pull/64#issuecomment-431507924
29-
(() async => jsify({'default': (await factory()).type}))(),
30-
),
31-
),
32-
),
33-
);
34-
3512
main() {
3613
final content = wrapper({});
3714

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

41-
final lazyComponent = lazy(() async {
42-
await simple.loadLibrary();
18+
final lazyComponent = react.lazy(() async {
4319
await Future.delayed(Duration(seconds: 5));
20+
await simple.loadLibrary();
21+
4422
return simple.SimpleComponent;
4523
});
4624

4725
var wrapper = react.registerFunctionComponent(WrapperComponent, displayName: 'wrapper');
4826

4927
WrapperComponent(Map props) {
28+
final showComponent = useState(false);
5029
return react.div({
5130
'id': 'lazy-wrapper'
5231
}, [
53-
react.Suspense({'fallback': 'Loading...'}, [lazyComponent({})])
32+
react.button({
33+
'onClick': (_) {
34+
showComponent.set(!showComponent.value);
35+
}
36+
}, 'Toggle component'),
37+
react.Suspense({'fallback': 'Loading...'}, showComponent.value ? lazyComponent({}) : null)
5438
]);
5539
}

lib/react.dart

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export 'package:react/src/context.dart';
2222
export 'package:react/src/prop_validator.dart';
2323
export 'package:react/src/react_client/event_helpers.dart';
2424
export 'package:react/react_client/react_interop.dart' show forwardRef2, createRef, memo2;
25+
export 'package:react/src/react_client/lazy.dart' show lazy;
2526
export 'package:react/src/react_client/synthetic_event_wrappers.dart' hide NonNativeDataTransfer;
2627
export 'package:react/src/react_client/synthetic_data_transfer.dart' show SyntheticDataTransfer;
2728

lib/react_client/react_interop.dart

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:react/react_client/js_backed_map.dart';
1818
import 'package:react/react_client/component_factory.dart' show ReactDartWrappedComponentFactoryProxy;
1919
import 'package:react/src/react_client/dart2_interop_workaround_bindings.dart';
2020
import 'package:react/src/typedefs.dart';
21+
import 'package:react/src/js_interop_util.dart';
2122

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

@@ -42,6 +43,7 @@ abstract class React {
4243
dynamic wrapperFunction, [
4344
bool Function(JsMap prevProps, JsMap nextProps)? areEqual,
4445
]);
46+
external static ReactClass lazy(Promise Function() load);
4547

4648
external static bool isValidElement(dynamic object);
4749

lib/src/react_client/lazy.dart

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import 'dart:js';
2+
import 'dart:js_util';
3+
4+
import 'package:react/react.dart';
5+
import 'package:react/react_client/component_factory.dart';
6+
import 'package:react/react_client/react_interop.dart';
7+
import 'package:react/src/js_interop_util.dart';
8+
9+
/// Defer loading a component's code until it is rendered for the first time.
10+
///
11+
/// 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.
12+
///
13+
/// 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.
14+
///
15+
/// Example usage:
16+
/// ```dart
17+
/// import 'package:react/react.dart' show lazy, Suspense;
18+
/// import './simple_component.dart' deferred as simple;
19+
///
20+
/// final lazyComponent = lazy(() async {
21+
/// await simple.loadLibrary();
22+
/// return simple.SimpleComponent;
23+
/// });
24+
///
25+
/// // Wrap the lazy component with Suspense
26+
/// final app = Suspense(
27+
/// {
28+
/// fallback: 'Loading...',
29+
/// },
30+
/// lazyComponent({}),
31+
/// );
32+
/// ```
33+
///
34+
/// Defer loading a component’s code until it is rendered for the first time.
35+
///
36+
/// Lazy components need to be wrapped with `Suspense` to render.
37+
/// `Suspense` also allows you to specify what should be displayed while the lazy component is loading.
38+
ReactComponentFactoryProxy lazy(Future<ReactComponentFactoryProxy> Function() load) {
39+
final hoc = React.lazy(
40+
allowInterop(
41+
() => futureToPromise(
42+
Future.sync(() async {
43+
final factory = await load();
44+
// By using a wrapper uiForwardRef it ensures that we have a matching factory proxy type given to react-dart's lazy,
45+
// a `ReactDartWrappedComponentFactoryProxy`. This is necessary to have consistent prop conversions since we don't
46+
// have access to the original factory proxy outside of this async block.
47+
final wrapper = forwardRef2((props, ref) {
48+
final children = props['children'];
49+
return factory.build(
50+
{...props, 'ref': ref},
51+
[
52+
if (children != null && !(children is List && children.isEmpty)) children,
53+
],
54+
);
55+
}, displayName: 'LazyWrapper(${_getComponentName(factory.type) ?? 'Anonymous'})');
56+
return jsify({'default': wrapper.type});
57+
}),
58+
),
59+
),
60+
);
61+
62+
// Setting this version and wrapping with ReactDartWrappedComponentFactoryProxy
63+
// is only okay because it matches the version and factory proxy of the wrapperFactory above.
64+
// ignore: invalid_use_of_protected_member
65+
setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2);
66+
return ReactDartWrappedComponentFactoryProxy(hoc);
67+
}
68+
69+
String? _getComponentName(Object? type) {
70+
if (type == null) return null;
71+
72+
if (type is String) return type;
73+
74+
final name = getProperty(type, 'name');
75+
if (name is String) return name;
76+
77+
final displayName = getProperty(type, 'displayName');
78+
if (displayName is String) return displayName;
79+
80+
return null;
81+
}

test/factory/common_factory_tests.dart

+32-17
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ import '../util.dart';
2626
/// [dartComponentVersion] should be specified for all components with Dart render code in order to
2727
/// properly test `props.children`, forwardRef compatibility, etc.
2828
void commonFactoryTests(ReactComponentFactoryProxy factory,
29-
{String? dartComponentVersion, bool skipPropValuesTest = false}) {
29+
{String? dartComponentVersion,
30+
bool skipPropValuesTest = false,
31+
bool isNonDartComponentWithDartWrapper = false,
32+
ReactElement Function(dynamic children)? renderWrapper}) {
3033
_childKeyWarningTests(
3134
factory,
32-
renderWithUniqueOwnerName: _renderWithUniqueOwnerName,
35+
renderWithUniqueOwnerName: (ReactElement Function() render) => _renderWithUniqueOwnerName(render, renderWrapper),
3336
);
3437

3538
test('renders an instance with the corresponding `type`', () {
@@ -113,7 +116,7 @@ void commonFactoryTests(ReactComponentFactoryProxy factory,
113116
shouldAlwaysBeList: isDartComponent2(factory({})));
114117
});
115118

116-
if (isDartComponent(factory({}))) {
119+
if (isDartComponent(factory({})) && !isNonDartComponentWithDartWrapper) {
117120
group('passes children to the Dart component when specified as', () {
118121
final notCalledSentinelValue = Object();
119122
dynamic childrenFromLastRender;
@@ -171,7 +174,7 @@ void commonFactoryTests(ReactComponentFactoryProxy factory,
171174
}
172175
}
173176

174-
if (isDartComponent2(factory({}))) {
177+
if (isDartComponent2(factory({})) && !isNonDartComponentWithDartWrapper) {
175178
test('executes Dart render code in the component zone', () {
176179
final oldComponentZone = componentZone;
177180
addTearDown(() => componentZone = oldComponentZone);
@@ -191,7 +194,10 @@ void commonFactoryTests(ReactComponentFactoryProxy factory,
191194
}
192195
}
193196

194-
void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) {
197+
void domEventHandlerWrappingTests(
198+
ReactComponentFactoryProxy factory, {
199+
bool isNonDartComponentWithDartWrapper = false,
200+
}) {
195201
Element renderAndGetRootNode(ReactElement content) {
196202
final mountNode = Element.div();
197203
react_dom.render(content, mountNode);
@@ -268,22 +274,31 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) {
268274
}
269275
});
270276

271-
if (isDartComponent(factory({}))) {
277+
if (isDartComponent(factory({})) && !isNonDartComponentWithDartWrapper) {
272278
group('in a way that the handlers are callable from within the Dart component:', () {
273279
setUpAll(() {
274280
expect(propsFromDartRender, isNotNull,
275281
reason: 'test setup: component must pass props into props.onDartRender');
276282
});
277283

278-
late react.SyntheticMouseEvent event;
279-
final divRef = react.createRef<DivElement>();
280-
render(react.div({
281-
'ref': divRef,
282-
'onClick': (react.SyntheticMouseEvent e) => event = e,
283-
}));
284-
rtu.Simulate.click(divRef);
284+
late react.SyntheticMouseEvent dummyEvent;
285+
setUpAll(() {
286+
final mountNode = DivElement();
287+
document.body!.append(mountNode);
288+
addTearDown(() {
289+
react_dom.unmountComponentAtNode(mountNode);
290+
mountNode.remove();
291+
});
285292

286-
final dummyEvent = event;
293+
final divRef = react.createRef<DivElement>();
294+
react_dom.render(
295+
react.div({
296+
'ref': divRef,
297+
'onClick': (react.SyntheticMouseEvent e) => dummyEvent = e,
298+
}),
299+
mountNode);
300+
divRef.current!.click();
301+
});
287302

288303
for (final eventCase in eventCases.where((helper) => helper.isDart)) {
289304
test(eventCase.description, () {
@@ -532,7 +547,7 @@ void _childKeyWarningTests(ReactComponentFactoryProxy factory,
532547
});
533548

534549
test('warns when a single child is passed as a list', () {
535-
_renderWithUniqueOwnerName(() => factory({}, [react.span({})]));
550+
renderWithUniqueOwnerName(() => factory({}, [react.span({})]));
536551

537552
expect(consoleErrorCalled, isTrue, reason: 'should have outputted a warning');
538553
expect(consoleErrorMessage, contains('Each child in a list should have a unique "key" prop.'));
@@ -577,12 +592,12 @@ int _nextFactoryId = 0;
577592
/// Renders the provided [render] function with a Component2 owner that will have a unique name.
578593
///
579594
/// This prevents React JS from not printing key warnings it deems as "duplicates".
580-
void _renderWithUniqueOwnerName(ReactElement Function() render) {
595+
void _renderWithUniqueOwnerName(ReactElement Function() render, [ReactElement Function(dynamic)? wrapper]) {
581596
final factory = react.registerComponent2(() => _UniqueOwnerHelperComponent());
582597
factory.reactClass.displayName = 'OwnerHelperComponent_$_nextFactoryId';
583598
_nextFactoryId++;
584599

585-
rtu.renderIntoDocument(factory({'render': render}));
600+
rtu.renderIntoDocument(factory({'render': wrapper != null ? () => wrapper(render()) : render}));
586601
}
587602

588603
class _UniqueOwnerHelperComponent extends react.Component2 {

0 commit comments

Comments
 (0)