├── .editorconfig ├── .github └── workflows │ └── dart_ci.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── COPYRIGHT_TRANFER ├── LICENSE ├── README.md ├── analysis_options.yaml ├── aviary.yaml ├── build.yaml ├── dart_test.yaml ├── example ├── README.md ├── geocodes │ ├── geocodes.dart │ └── geocodes.html ├── index.html ├── js_components │ ├── index.html │ ├── js_components.dart │ └── js_components.js ├── suspense │ ├── index.html │ ├── simple_component.dart │ └── suspense.dart └── test │ ├── call_and_nosuchmethod_test.dart │ ├── call_and_nosuchmethod_test.html │ ├── context_test.dart │ ├── context_test.html │ ├── error_boundary_test.dart │ ├── error_boundary_test.html │ ├── false_and_null_test.dart │ ├── false_and_null_test.html │ ├── function_component_test.dart │ ├── function_component_test.html │ ├── get_dom_node_test.dart │ ├── get_dom_node_test.html │ ├── order_test.dart │ ├── order_test.html │ ├── react_test.dart │ ├── react_test.html │ ├── react_test_components.dart │ ├── ref_test.dart │ ├── ref_test.html │ ├── speed_test.dart │ ├── speed_test.html │ ├── unmount_test.dart │ └── unmount_test.html ├── js_src ├── _dart_helpers.js ├── dart_env_dev.mjs ├── index.mjs ├── react.mjs ├── react_dom.mjs └── react_dom_dev.mjs ├── lib ├── hooks.dart ├── js │ ├── react.dev.js │ ├── react.min.js │ └── react.min.js.map ├── react.dart ├── react.js ├── react.js.map ├── react_client.dart ├── react_client │ ├── bridge.dart │ ├── component_factory.dart │ ├── js_backed_map.dart │ ├── js_interop_helpers.dart │ ├── react_interop.dart │ └── zone.dart ├── react_dom.dart ├── react_dom.js ├── react_dom.js.map ├── react_dom_prod.js ├── react_dom_prod.js.map ├── react_dom_server.dart ├── react_dom_server.js ├── react_dom_server.js.map ├── react_dom_server_prod.js ├── react_dom_server_prod.js.map ├── react_prod.js ├── react_prod.js.map ├── react_test_utils.dart ├── react_with_addons.js ├── react_with_addons.js.map ├── react_with_react_dom_prod.js ├── react_with_react_dom_prod.js.map └── src │ ├── context.dart │ ├── js_interop_util.dart │ ├── prop_validator.dart │ ├── react_client │ ├── chain_refs.dart │ ├── component_registration.dart │ ├── dart2_interop_workaround_bindings.dart │ ├── dart_interop_statics.dart │ ├── event_helpers.dart │ ├── event_prop_key_to_event_factory.dart │ ├── factory_util.dart │ ├── internal_react_interop.dart │ ├── lazy.dart │ ├── private_utils.dart │ ├── synthetic_data_transfer.dart │ └── synthetic_event_wrappers.dart │ ├── react_test_utils │ └── simulate_wrappers.dart │ └── typedefs.dart ├── package.json ├── pnpm-lock.yaml ├── pubspec.yaml ├── test ├── ReactCompositeTestComponent.js ├── ReactSetStateTestComponent.js ├── ReactSetStateTestComponent2.js ├── factory │ ├── common_factory_tests.dart │ ├── dart_factory_test.dart │ ├── dart_function_factory_test.dart │ ├── dom_factory_test.dart │ └── js_factory_test.dart ├── forward_ref_test.dart ├── hooks_test.dart ├── js_builds │ ├── react17 │ │ ├── README │ │ ├── console_spy_include_this_js_first.js │ │ ├── js_dev_test.custom.html │ │ ├── js_dev_test.dart │ │ ├── js_dev_with_addons_test.custom.html │ │ ├── js_dev_with_addons_test.dart │ │ ├── js_prod_combined_test.custom.html │ │ ├── js_prod_combined_test.dart │ │ ├── js_prod_test.custom.html │ │ └── js_prod_test.dart │ ├── react18 │ │ ├── README │ │ ├── console_spy_include_this_js_first.js │ │ ├── js_dev_test.custom.html │ │ ├── js_dev_test.dart │ │ ├── js_prod_test.custom.html │ │ └── js_prod_test.dart │ └── shared_tests.dart ├── lifecycle_test.dart ├── lifecycle_test │ ├── component.dart │ ├── component2.dart │ └── util.dart ├── nullable_callback_detection │ ├── non_null_safe_refs.dart │ ├── null_safe_refs.dart │ ├── nullable_callback_detection_sound_test.dart │ ├── nullable_callback_detection_unsound_test.dart │ └── sound_null_safety_detection.dart ├── react_client │ ├── bridge_test.dart │ ├── chain_refs_test.dart │ ├── event_helpers_test.dart │ ├── js_backed_map_test.dart │ ├── js_interop_helpers_test.dart │ └── react_interop_test.dart ├── react_client_test.dart ├── react_component_test.dart ├── react_context_test.dart ├── react_dom_test.dart ├── react_fragment_test.dart ├── react_lazy_test.dart ├── react_memo_test.dart ├── react_strictmode_test.dart ├── react_suspense_lazy_component.dart ├── react_suspense_test.dart ├── react_test_utils_test.dart ├── react_version_test.dart ├── shared_type_tester.dart ├── templates │ ├── html_template-react17.html │ └── html_template.html ├── test_components.dart ├── test_components2.dart ├── util.dart └── util_test.dart ├── tool ├── dart_dev │ └── config.dart ├── delete_dart_2_only_files.sh └── run_consumer_tests.dart └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.dart] 2 | max_line_length = 120 3 | 4 | [*.js] 5 | indent_size = 2 6 | max_line_length = 120 7 | -------------------------------------------------------------------------------- /.github/workflows/dart_ci.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'test_consume_*' 8 | pull_request: 9 | branches: 10 | - '**' 11 | 12 | jobs: 13 | # Run as a separate job outside the Dart SDK matrix below, 14 | # since we can only emit a single SBOM. 15 | create-sbom-release-asset: 16 | name: Create SBOM Release Asset 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dart-lang/setup-dart@v1 21 | with: 22 | sdk: 2.19.6 # This version doesn't matter so long as it resolves. 23 | - run: dart pub get 24 | - name: Publish SBOM to Release Assets 25 | uses: anchore/sbom-action@v0 26 | with: 27 | path: ./ 28 | format: cyclonedx-json 29 | 30 | build: 31 | runs-on: ubuntu-latest 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | react: [17, 18] 36 | sdk: [2.19.6, stable] 37 | steps: 38 | - uses: actions/checkout@v2 39 | - id: setup-dart 40 | uses: dart-lang/setup-dart@v1 41 | with: 42 | sdk: ${{ matrix.sdk }} 43 | 44 | - name: Print Dart SDK version 45 | run: dart --version 46 | 47 | - name: Delete Dart-2-only files when running on Dart 3 48 | run: | 49 | DART_VERSION="${{ steps.setup-dart.outputs.dart-version }}" 50 | if [[ "$DART_VERSION" =~ ^3 ]]; then 51 | ./tool/delete_dart_2_only_files.sh 52 | fi 53 | - name: Switch to React 17 Test HTML 54 | if: ${{ matrix.react == 17 }} 55 | run: | 56 | mv test/templates/html_template.html test/templates/html_template-old.html 57 | mv test/templates/html_template-react17.html test/templates/html_template.html 58 | 59 | - id: install 60 | name: Install dependencies 61 | run: dart pub get 62 | 63 | - name: Validate dependencies 64 | run: dart run dependency_validator 65 | if: always() && steps.install.outcome == 'success' 66 | 67 | - name: Verify formatting 68 | run: dart run dart_dev format --check 69 | if: ${{ matrix.sdk == '2.19.6' }} 70 | 71 | - name: Analyze project source 72 | run: dart analyze 73 | if: always() && steps.install.outcome == 'success' 74 | 75 | - name: Run tests (DDC) 76 | run: | 77 | dart run build_runner test --delete-conflicting-outputs -- --preset dartdevc --preset=react${{ matrix.react }} 78 | if: always() && steps.install.outcome == 'success' 79 | timeout-minutes: 8 80 | 81 | - name: Run tests (dart2js) 82 | run: | 83 | dart run build_runner test --delete-conflicting-outputs --release -- --preset dart2js --preset=react${{ matrix.react }} 84 | if: always() && steps.install.outcome == 'success' 85 | timeout-minutes: 8 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | .buildlog 3 | lib/docs/ 4 | pubspec.lock 5 | .idea 6 | .pub/ 7 | .packages 8 | node_modules/ 9 | .dart_tool 10 | .vscode 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Samuel Hapak 2 | -------------------------------------------------------------------------------- /COPYRIGHT_TRANFER: -------------------------------------------------------------------------------- 1 | I, Samuel Hapak, hereby transfer copyright of react-dart to Workiva Inc. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, vacuumapps s.r.o. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:workiva_analysis_options/v2.recommended.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | errors: 7 | must_call_super: error 8 | comment_references: info 9 | # This is too noisy since it warns for all lifecycle methods. 10 | always_declare_return_types: ignore 11 | # We very often need to annotate parameters when they're embedded in Maps 12 | # with dynamic values, so this lint is noisy and wrong in those cases. 13 | # See: https://github.com/dart-lang/linter/issues/1099 14 | avoid_types_on_closure_parameters: ignore 15 | # This makes string concatenation less readable in some cases 16 | prefer_interpolation_to_compose_strings: ignore 17 | # The following are ignored to avoid merge conflicts with null safety branch 18 | directives_ordering: ignore 19 | prefer_typing_uninitialized_variables: ignore 20 | linter: 21 | rules: 22 | prefer_if_elements_to_conditional_expressions: false 23 | overridden_fields: false 24 | type_annotate_public_apis: false 25 | -------------------------------------------------------------------------------- /aviary.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | exclude: 4 | - ^example/ 5 | - ^test/ 6 | - ^tool/ 7 | # Ignore compiled JS files and source maps 8 | - ^lib/react\w*\.js(\.map)?$ 9 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | test_html_builder: 5 | options: 6 | templates: 7 | "test/templates/html_template.html": 8 | - "test/**_test.dart" 9 | build_web_compilers|entrypoint: 10 | # These are globs for the entrypoints you want to compile. 11 | generate_for: 12 | - test/**.browser_test.dart 13 | - example/**.dart 14 | options: 15 | # List any dart2js specific args here, or omit it. 16 | dart2js_args: 17 | # Omit type checks TODO change to -O4 at some point (e.g., --trust-primitives) 18 | - -O3 19 | # Generate CSP-compliant code since it will be used most often in prod 20 | - --csp 21 | # Disable minification for easier test/example debugging 22 | - --no-minify 23 | 24 | -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | # Specify chrome and VM as default platforms for running all tests, 2 | # then let the `@TestOn()` annotations determine which suites actually run 3 | platforms: 4 | - chrome 5 | - vm 6 | 7 | # Work around process handing after tests finish: https://github.com/dart-lang/build/issues/3765 8 | concurrency: 1 9 | 10 | presets: 11 | dart2js: 12 | exclude_tags: no-dart2js 13 | 14 | dartdevc: 15 | exclude_tags: no-dartdevc 16 | 17 | react18: 18 | exclude_tags: react-17 19 | 20 | react17: 21 | exclude_tags: react-18 22 | 23 | tags: 24 | # Variadic children tests of >5 children that fail in Dart 2.7 for an unknown reason, seemingly an SDK bug. 25 | # These tests pass in later Dart SDKs, so we ignore them when running in 2.7. 26 | "dart-2-7-dart2js-variadic-issues": 27 | "react-17": 28 | "react-18": 29 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Serving / Viewing React Dart Examples 2 | 3 | 1. Run `dart pub get` in your terminal. 4 | 2. Run `webdev serve example`. 5 | 2. Open Chrome. 6 | 3. Navigate to 7 | -------------------------------------------------------------------------------- /example/geocodes/geocodes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | React Dart Examples: Geocode Resolver 27 | 28 | 29 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples 10 | 11 | 12 |
13 |

React Dart Examples

14 | 15 |

App Example

16 | 17 | 22 | 23 |

Use Case / Test Examples

24 | 25 |

These are mostly here for the benefit of library contributors, but if you want to poke around - that's fine by us!

26 | 27 | 65 | 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /example/js_components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JS Components - react-dart Examples 6 | 13 | 14 | 15 | See Dart source for more info 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/js_components/js_components.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library js_components; 3 | 4 | import 'dart:html'; 5 | 6 | import 'package:js/js.dart'; 7 | import 'package:react/react.dart' as react; 8 | import 'package:react/react_client.dart'; 9 | import 'package:react/react_client/react_interop.dart'; 10 | import 'package:react/react_dom.dart' as react_dom; 11 | 12 | main() { 13 | final content = IndexComponent({}); 14 | 15 | react_dom.render(content, querySelector('#content')); 16 | } 17 | 18 | var IndexComponent = react.registerComponent2(() => _IndexComponent()); 19 | 20 | class _IndexComponent extends react.Component2 { 21 | SimpleCustomComponent? simpleRef; 22 | 23 | @override 24 | get initialState => { 25 | 'open': false, 26 | }; 27 | 28 | handleClose(_) { 29 | setState({ 30 | 'open': false, 31 | }); 32 | } 33 | 34 | handleClick(_) { 35 | setState({ 36 | 'open': true, 37 | }); 38 | print(simpleRef!.getFoo()); 39 | } 40 | 41 | @override 42 | render() { 43 | return MuiThemeProvider( 44 | { 45 | 'theme': theme, 46 | }, 47 | SimpleCustom({ 48 | 'foo': 'Foo Prop from dart... IN A JAVASCRIPT COMPONENT!', 49 | 'ref': (ref) { 50 | simpleRef = ref as SimpleCustomComponent; 51 | } 52 | }), 53 | CssBaseline({}), 54 | Dialog( 55 | { 56 | 'open': state['open'], 57 | 'onClose': handleClose, 58 | }, 59 | DialogTitle({}, 'Super Secret Password'), 60 | DialogContent( 61 | {}, 62 | DialogContentText({}, '1-2-3-4-5'), 63 | ), 64 | DialogActions( 65 | {}, 66 | Button({ 67 | 'color': 'primary', 68 | 'onClick': handleClose, 69 | }, 'OK'), 70 | )), 71 | Typography({ 72 | 'variant': 'h4', 73 | 'gutterBottom': true, 74 | }, 'Material-UI'), 75 | Typography({ 76 | 'variant': 'subtitle1', 77 | 'gutterBottom': true, 78 | }, 'example project'), 79 | Button({ 80 | 'variant': 'contained', 81 | 'color': 'secondary', 82 | 'onClick': handleClick, 83 | }, Icon({}, 'fingerprint'), 'Super Secret Password'), 84 | ); 85 | } 86 | } 87 | 88 | /// The JS component class. 89 | /// 90 | /// Private since [SimpleCustomComponent] will be used instead. 91 | /// 92 | /// Accessible via `SimpleCustomComponent.type`. 93 | @JS() 94 | external ReactClass get _SimpleCustomComponent; 95 | 96 | /// A factory for the "_SimpleJsModalContent" JS components class that allows it 97 | /// to be used via Dart code. 98 | /// 99 | /// Use this to render instances of the component from within Dart code. 100 | /// 101 | /// This converts the Dart props [Map] passed into it in the 102 | /// the same way props are converted for DOM components. 103 | final SimpleCustom = ReactJsComponentFactoryProxy(_SimpleCustomComponent); 104 | 105 | /// JS interop wrapper class for the component, 106 | /// allowing us to interact with component instances 107 | /// made available via refs or [react_dom.render] calls. 108 | /// 109 | /// This is optional, as you won't always need to access the component's API. 110 | @JS('_SimpleCustomComponent') 111 | class SimpleCustomComponent { 112 | external String getFoo(); 113 | } 114 | 115 | /// JS interop wrapper class for Material UI 116 | /// getting us access to the react classes 117 | @JS() 118 | class MaterialUI { 119 | external static ReactClass get Button; 120 | external static ReactClass get CssBaseline; 121 | external static ReactClass get Dialog; 122 | external static ReactClass get DialogActions; 123 | external static ReactClass get DialogContent; 124 | external static ReactClass get DialogContentText; 125 | external static ReactClass get DialogTitle; 126 | external static ReactClass get Icon; 127 | external static ReactClass get MuiThemeProvider; 128 | external static ReactClass get Typography; 129 | } 130 | 131 | /// JS Interop to get theme config we setup in JS 132 | @JS() 133 | external Map get theme; 134 | 135 | // All the Material UI components converted to dart Components 136 | final Button = ReactJsComponentFactoryProxy(MaterialUI.Button); 137 | final CssBaseline = ReactJsComponentFactoryProxy(MaterialUI.CssBaseline); 138 | final Dialog = ReactJsComponentFactoryProxy(MaterialUI.Dialog); 139 | final DialogActions = ReactJsComponentFactoryProxy(MaterialUI.DialogActions); 140 | final DialogContent = ReactJsComponentFactoryProxy(MaterialUI.DialogContent); 141 | final DialogContentText = ReactJsComponentFactoryProxy(MaterialUI.DialogContentText); 142 | final DialogTitle = ReactJsComponentFactoryProxy(MaterialUI.DialogTitle); 143 | final Icon = ReactJsComponentFactoryProxy(MaterialUI.Icon); 144 | final MuiThemeProvider = ReactJsComponentFactoryProxy(MaterialUI.MuiThemeProvider); 145 | final Typography = ReactJsComponentFactoryProxy(MaterialUI.Typography); 146 | -------------------------------------------------------------------------------- /example/js_components/js_components.js: -------------------------------------------------------------------------------- 1 | // Lets use some of material ui's stuff in JS world 2 | const { 3 | createMuiTheme, 4 | colors 5 | } = window.MaterialUI; 6 | 7 | // Assigned to the window so that dart can get access to it. 8 | window.theme = createMuiTheme({ 9 | palette: { 10 | primary: { 11 | light: colors.purple[300], 12 | main: colors.purple[500], 13 | dark: colors.purple[700] 14 | }, 15 | secondary: { 16 | light: colors.green[300], 17 | main: colors.green[500], 18 | dark: colors.green[700] 19 | } 20 | }, 21 | typography: { 22 | useNextVariants: true 23 | } 24 | }); 25 | 26 | // Assigned to the window so that dart can get access to it. 27 | window._SimpleCustomComponent = class _SimpleCustomComponent extends React.Component { 28 | render(){ 29 | return 'This is some content to show that custom JS components also work!'; 30 | } 31 | getFoo(){ 32 | return this.props.foo 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/suspense/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Suspense - react-dart Examples 6 | 13 | 14 | 15 | See Dart source for more info 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /example/suspense/simple_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:react/hooks.dart'; 2 | import 'package:react/react.dart' as react; 3 | 4 | var SimpleComponent = react.registerFunctionComponent(SimpleFunctionComponent, displayName: 'simple'); 5 | 6 | SimpleFunctionComponent(Map props) { 7 | final count = useState(1); 8 | final evenOdd = useState('even'); 9 | 10 | useEffect(() { 11 | if (count.value % 2 == 0) { 12 | print('count changed to ' + count.value.toString()); 13 | evenOdd.set('even'); 14 | } else { 15 | print('count changed to ' + count.value.toString()); 16 | evenOdd.set('odd'); 17 | } 18 | return () { 19 | print('count is changing... do some cleanup if you need to'); 20 | }; 21 | 22 | /// This dependency prevents the effect from running every time [evenOdd.value] changes. 23 | }, [count.value]); 24 | 25 | return react.div({}, [ 26 | react.button({'onClick': (_) => count.set(1), 'key': 'ust1'}, ['Reset']), 27 | react.button({'onClick': (_) => count.setWithUpdater((prev) => prev + 1), 'key': 'ust2'}, ['+']), 28 | react.br({'key': 'ust3'}), 29 | react.p({'key': 'ust4'}, ['${count.value} is ${evenOdd.value}']), 30 | ]); 31 | } 32 | -------------------------------------------------------------------------------- /example/suspense/suspense.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library example.suspense.suspense; 3 | 4 | import 'dart:html'; 5 | 6 | import 'package:js/js.dart'; 7 | import 'package:react/hooks.dart'; 8 | import 'package:react/react.dart' as react; 9 | import 'package:react/react_dom.dart' as react_dom; 10 | import './simple_component.dart' deferred as simple; 11 | 12 | main() { 13 | final content = wrapper({}); 14 | 15 | react_dom.render(content, querySelector('#content')); 16 | } 17 | 18 | final lazyComponent = react.lazy(() async { 19 | await Future.delayed(Duration(seconds: 5)); 20 | await simple.loadLibrary(); 21 | 22 | return simple.SimpleComponent; 23 | }); 24 | 25 | var wrapper = react.registerFunctionComponent(WrapperComponent, displayName: 'wrapper'); 26 | 27 | WrapperComponent(Map props) { 28 | final showComponent = useState(false); 29 | return react.div({ 30 | 'id': 'lazy-wrapper' 31 | }, [ 32 | react.button({ 33 | 'onClick': (_) { 34 | showComponent.set(!showComponent.value); 35 | } 36 | }, 'Toggle component'), 37 | react.Suspense({'fallback': 'Loading...'}, showComponent.value ? lazyComponent({}) : null) 38 | ]); 39 | } 40 | -------------------------------------------------------------------------------- /example/test/call_and_nosuchmethod_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'dart:html'; 3 | 4 | import 'package:react/react_dom.dart' as react_dom; 5 | import 'package:react/react.dart' as react; 6 | 7 | class _CustomComponent extends react.Component { 8 | @override 9 | render() { 10 | return react.div({}, props['children']); 11 | } 12 | } 13 | 14 | var customComponent = react.registerComponent(() => _CustomComponent()); 15 | 16 | void main() { 17 | react_dom.render( 18 | react.div({}, [ 19 | react.div({'key': 'noChildren'}), 20 | react.div({'key': 'emptyList'}, []), 21 | react.div({'key': 'singleItemInList'}, [react.div({})]), // This should produce a key warning 22 | react.div({'key': 'twoItemsInList'}, [react.div({}), react.div({})]), // This should produce a key warning 23 | react.div({'key': 'oneVariadicChild'}, react.div({})), 24 | 25 | // These tests of variadic children won't pass in the ddc until https://github.com/dart-lang/sdk/issues/29904 26 | // is resolved. 27 | // react.div({'key': 'twoVariadicChildren'}, react.div({}), react.div({})), 28 | // react.div({'key': 'fiveVariadicChildren'}, '', react.div({}), '', react.div({}), ''), 29 | 30 | customComponent({'key': 'noChildren2'}), 31 | customComponent({'key': 'emptyList2'}, []), 32 | customComponent({'key': 'singleItemInList2'}, [customComponent({})]), // This should produce a key warning 33 | customComponent({'key': 'twoItemsInList2'}, 34 | [customComponent({}), customComponent({})]), // This should produce a key warning 35 | customComponent({'key': 'oneVariadicChild2'}, customComponent({'key': '1'})), 36 | 37 | // These tests of variadic children won't pass in the ddc until https://github.com/dart-lang/sdk/issues/29904 38 | // is resolved. 39 | // customComponent({'key': 'twoVariadicChildren2'}, customComponent({}), customComponent({})), 40 | // customComponent({'key': 'fiveVariadicChildren2'}, '', customComponent({}), '', customComponent({}), ''), 41 | ]), 42 | querySelector('#content')); 43 | } 44 | -------------------------------------------------------------------------------- /example/test/call_and_nosuchmethod_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: call and noSuchMethod test 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/test/context_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:react/react.dart' as react; 4 | import 'package:react/react_dom.dart' as react_dom; 5 | 6 | import 'react_test_components.dart'; 7 | 8 | void main() { 9 | react_dom.render( 10 | react.div({}, [ 11 | react.h1({}, ['React Legacy Context API']), 12 | legacyContextComponent({}, [ 13 | legacyContextConsumerComponent({ 14 | 'key': 'consumerComponent' 15 | }, [ 16 | grandchildLegacyContextConsumerComponent({'key': 'legacyConsumerGrandchildComponent'}) 17 | ]), 18 | ]), 19 | react.h1({'key': 'h1'}, ['React New Context API']), 20 | newContextProviderComponent({ 21 | 'key': 'newProviderComponent' 22 | }, [ 23 | newContextConsumerComponent({ 24 | 'key': 'newConsumerComponent', 25 | }), 26 | newContextConsumerObservedBitsComponent({ 27 | 'key': 'newConsumerObservedBitsComponent', 28 | 'unstable_observedBits': 1 << 2, 29 | }), 30 | newContextTypeConsumerComponentComponent({'key': 'newContextTypeConsumerComponent'}, []), 31 | ]), 32 | ]), 33 | querySelector('#content')); 34 | } 35 | -------------------------------------------------------------------------------- /example/test/context_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: context test 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/test/error_boundary_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:react/react.dart' as react; 4 | import 'package:react/react_client/react_interop.dart'; 5 | import 'package:react/react_dom.dart' as react_dom; 6 | 7 | const containerStyle = { 8 | 'border': '1px solid black', 9 | 'margin': 8, 10 | 'padding': 8, 11 | }; 12 | 13 | void main() { 14 | final content = react.div( 15 | {}, 16 | _ErrorBoundary( 17 | { 18 | 'onComponentDidCatch': (dynamic error, ReactErrorInfo info) { 19 | print('componentDidCatch: info.componentStack ${info.componentStack}'); 20 | }, 21 | }, 22 | react.div( 23 | {'style': containerStyle}, 24 | '(Inside an error boundary)', 25 | _ThrowingComponent({}), 26 | ), 27 | ), 28 | _ErrorBoundary( 29 | {}, 30 | react.div( 31 | {'style': containerStyle}, 32 | '(Outside an error boundary)', 33 | _ThrowingComponent({}), 34 | ), 35 | ), 36 | ); 37 | 38 | react_dom.render(content, querySelector('#content')); 39 | } 40 | 41 | final _ErrorBoundary = react.registerComponent2(() => _ErrorBoundaryComponent(), skipMethods: []); 42 | 43 | class _ErrorBoundaryComponent extends react.Component2 { 44 | @override 45 | get initialState => {'hasError': false}; 46 | 47 | @override 48 | getDerivedStateFromError(dynamic error) => {'hasError': true}; 49 | 50 | @override 51 | componentDidCatch(dynamic error, ReactErrorInfo info) { 52 | props['onComponentDidCatch']?.call(error, info); 53 | } 54 | 55 | @override 56 | render() { 57 | return (state['hasError'] as bool) ? 'Error boundary caught an error' : props['children']; 58 | } 59 | } 60 | 61 | final _ThrowingComponent = react.registerComponent2(() => _ThrowingComponentComponent2()); 62 | 63 | class _ThrowingComponentComponent2 extends react.Component2 { 64 | @override 65 | get initialState => {'shouldThrow': false}; 66 | 67 | @override 68 | render() { 69 | if (state['shouldThrow'] as bool) { 70 | throw Exception(); 71 | } 72 | 73 | return react.button({ 74 | 'type': 'button', 75 | 'onClick': (_) { 76 | setState({'shouldThrow': true}); 77 | } 78 | }, 'Click me to throw'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/test/error_boundary_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: error boundary test 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/test/false_and_null_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'dart:html'; 3 | 4 | import 'package:react/react.dart' as react; 5 | import 'package:react/react_dom.dart' as react_dom; 6 | 7 | class _Component extends react.Component { 8 | @override 9 | render() { 10 | if (props['hardCodedNullReturn'] == true) { 11 | return null; 12 | } 13 | 14 | return props['returnValue']; 15 | } 16 | } 17 | 18 | var component = react.registerComponent(() => _Component()); 19 | 20 | void main() { 21 | final content = react.div({}, [ 22 | react.p({}, 'Testing a dynamic return value of "null"...'), 23 | component({'returnValue': null, 'key': 0}), 24 | react.p({}, 'Testing a hard-coded return value of "null"...'), 25 | component({'hardCodedNullReturn': true, 'key': 1}), 26 | react.p({}, 'Testing a return value of "false"...'), 27 | component({'returnValue': false, 'key': 2}), 28 | ]); 29 | 30 | react_dom.render(content, querySelector('#content')); 31 | } 32 | -------------------------------------------------------------------------------- /example/test/false_and_null_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: false and null 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/test/function_component_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: function component test 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/test/get_dom_node_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'dart:html'; 3 | 4 | import 'package:react/react.dart' as react; 5 | import 'package:react/react_dom.dart' as react_dom; 6 | 7 | // ignore: avoid_positional_boolean_parameters 8 | void customAssert(String text, bool condition) { 9 | if (condition) { 10 | print('$text passed'); 11 | } else { 12 | throw Exception(text); 13 | } 14 | } 15 | 16 | var ChildComponent = react.registerComponent(() => _ChildComponent()); 17 | 18 | class _ChildComponent extends react.Component { 19 | var counter = 0; 20 | 21 | @override 22 | render() => react.div({}, [ 23 | 'Test element', 24 | counter.toString(), 25 | react.button({ 26 | 'type': 'button', 27 | 'key': 'button', 28 | 'className': 'btn btn-primary', 29 | 'onClick': (_) { 30 | counter++; 31 | redraw(); 32 | }, 33 | }, 'Increase counter') 34 | ]); 35 | } 36 | 37 | var simpleComponent = react.registerComponent(() => SimpleComponent()); 38 | 39 | class SimpleComponent extends react.Component { 40 | var refToSpan; 41 | var refToElement; 42 | 43 | @override 44 | componentWillMount() => print('mount'); 45 | 46 | @override 47 | componentWillUnmount() => print('unmount'); 48 | 49 | @override 50 | componentDidMount() { 51 | customAssert('ref to span return span ', refToSpan.text == 'Test'); 52 | customAssert('findDOMNode works on this', react_dom.findDOMNode(this) != null); 53 | customAssert('random ref resolves to null', ref('someRandomRef') == null); 54 | } 55 | 56 | var counter = 0; 57 | 58 | @override 59 | render() => react.div({}, [ 60 | react.span({ 61 | 'key': 'span1', 62 | 'ref': (ref) { 63 | refToSpan = ref; 64 | } 65 | }, 'Test'), 66 | react.span({'key': 'span2'}, counter), 67 | react.button({ 68 | 'type': 'button', 69 | 'key': 'button1', 70 | 'className': 'btn btn-primary', 71 | 'onClick': (_) => (react_dom.findDOMNode(this) as HtmlElement).children.first.text = (++counter).toString() 72 | }, 'Increase counter'), 73 | react.br({'key': 'br'}), 74 | ChildComponent({ 75 | 'key': 'child', 76 | 'ref': (ref) { 77 | refToElement = ref; 78 | } 79 | }), 80 | react.button({ 81 | 'type': 'button', 82 | 'key': 'button2', 83 | 'className': 'btn btn-primary', 84 | 'onClick': (_) => window.alert(refToElement.counter.toString()) 85 | }, 'Show value of child element'), 86 | ]); 87 | } 88 | 89 | var mountedNode = querySelector('#content'); 90 | 91 | void main() { 92 | final component = simpleComponent({}); 93 | react_dom.render(component, mountedNode); 94 | } 95 | -------------------------------------------------------------------------------- /example/test/get_dom_node_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: getDomNode test 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/test/order_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'dart:html'; 3 | 4 | import 'package:react/react.dart' as react; 5 | import 'package:react/react_dom.dart' as react_dom; 6 | 7 | class _Item extends react.Component { 8 | @override 9 | componentWillReceiveProps(newProps) { 10 | print('Old props: $props'); 11 | print('New props: $newProps'); 12 | } 13 | 14 | @override 15 | shouldComponentUpdate(nextProps, nextState) { 16 | return false; 17 | } 18 | 19 | @override 20 | render() { 21 | return react.li({}, [props['text']]); 22 | } 23 | } 24 | 25 | var item = react.registerComponent(() => _Item()); 26 | 27 | class _List extends react.Component { 28 | var items = ['item1', 'item2', 'item3']; 29 | 30 | remove() { 31 | items.removeAt(0); 32 | redraw(); 33 | } 34 | 35 | @override 36 | render() { 37 | return react.ul({'onClick': (e) => remove()}, items.map((text) => item({'text': text, 'key': text}))); 38 | } 39 | } 40 | 41 | var list = react.registerComponent(() => _List()); 42 | 43 | void main() { 44 | react_dom.render(list({}), querySelector('#content')); 45 | } 46 | -------------------------------------------------------------------------------- /example/test/order_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: order test 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/test/react_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:react/react_dom.dart' as react_dom; 4 | 5 | import 'react_test_components.dart'; 6 | 7 | void main() { 8 | react_dom.render( 9 | mainComponent({}, [ 10 | helloGreeter({'key': 'hello'}, []), 11 | listComponent({'key': 'list'}, []), 12 | component2TestComponent({'key': 'c2-list'}, []), 13 | component2ErrorTestComponent({'key': 'error-boundary'}, []), 14 | //clockComponent({"name": 'my-clock'}, []), 15 | checkBoxComponent({'key': 'checkbox'}, []) 16 | ]), 17 | querySelector('#content')); 18 | } 19 | -------------------------------------------------------------------------------- /example/test/react_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: react test 10 | 15 | 16 | 17 | 18 | 19 |
Hello World!
  • 0
  • 1
  • 2
  • 3
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/test/ref_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: ref test 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/test/speed_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'dart:html'; 3 | import 'dart:async'; 4 | 5 | import 'package:react/react.dart' as react; 6 | import 'package:react/react_dom.dart' as react_dom; 7 | 8 | Stopwatch stopwatch = Stopwatch()..start(); 9 | timeprint(message) { 10 | print('$message ${stopwatch.elapsedMilliseconds}'); 11 | stopwatch.reset(); 12 | } 13 | 14 | class _Div extends react.Component { 15 | @override 16 | shouldComponentUpdate(nProps, nState) { 17 | return nProps['key'] != props['key']; 18 | } 19 | 20 | @override 21 | render() { 22 | return react.div(props, props['children']); 23 | } 24 | } 25 | 26 | var Div = react.registerComponent(() => _Div()); 27 | 28 | class _Span extends react.Component { 29 | @override 30 | shouldComponentUpdate(nProps, nState) { 31 | return nProps['children'][0] != props['children'][0]; 32 | } 33 | 34 | @override 35 | render() { 36 | return react.span(props, props['children']); 37 | } 38 | } 39 | 40 | var Span = react.registerComponent(() => _Span()); 41 | 42 | class _Hello extends react.Component { 43 | @override 44 | componentWillMount() { 45 | Future.delayed(Duration(seconds: 5), () { 46 | stopwatch.reset(); 47 | timeprint('before redraw call'); 48 | redraw(); 49 | timeprint('after redraw call'); 50 | }); 51 | } 52 | 53 | @override 54 | render() { 55 | timeprint('rendering start'); 56 | final data = props['data'] as List; 57 | final children = []; 58 | for (final elem in data) { 59 | children.add(react.div({ 60 | 'key': elem[0] 61 | }, [ 62 | react.span({'key': 'span1'}, elem[0]), 63 | ' ', 64 | react.span({'key': 'span2'}, elem[1]) 65 | ])); 66 | } 67 | // data.forEach((elem) => children.add( 68 | // react.div({'key': elem[0]},[ 69 | // react.span({}, elem[0]), 70 | // " ", 71 | // react.span({}, elem[1]) 72 | // ])) 73 | // ); 74 | timeprint('rendering almost ends'); 75 | final res = react.div({}, children); 76 | timeprint('rendering ends'); 77 | return res; 78 | } 79 | } 80 | 81 | var Hello = react.registerComponent(() => _Hello()); 82 | 83 | void main() { 84 | final data = []; 85 | for (num i = 0; i < 1000; i++) { 86 | data.add(['name_$i', 'value_$i']); 87 | } 88 | react_dom.render(Hello({'data': data}, []), querySelector('#content')); 89 | } 90 | -------------------------------------------------------------------------------- /example/test/speed_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: speed test 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/test/unmount_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'dart:html'; 3 | 4 | import 'package:react/react.dart' as react; 5 | import 'package:react/react_dom.dart' as react_dom; 6 | 7 | var simpleComponent = react.registerComponent(() => SimpleComponent()); 8 | 9 | class SimpleComponent extends react.Component { 10 | @override 11 | componentWillMount() => print('mount'); 12 | 13 | @override 14 | componentWillUnmount() => print('unmount'); 15 | 16 | @override 17 | render() => react.div({}, [ 18 | 'Simple component', 19 | ]); 20 | } 21 | 22 | void main() { 23 | print('What'); 24 | final mountedNode = querySelector('#content'); 25 | 26 | querySelector('#mount')!.onClick.listen((_) => react_dom.render(simpleComponent({}), mountedNode)); 27 | 28 | querySelector('#unmount')!.onClick.listen((_) => react_dom.unmountComponentAtNode(mountedNode)); 29 | } 30 | -------------------------------------------------------------------------------- /example/test/unmount_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Dart Examples: unmount test 10 | 11 | 12 | 13 |
14 |
15 | 16 |   17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /js_src/dart_env_dev.mjs: -------------------------------------------------------------------------------- 1 | // Intercept console.error calls and silence warnings for each react_dom.render call, 2 | // until at the very least createRoot is made available in react-dart and RTL. 3 | const oldConsoleError = console.error; 4 | console.error = function() { 5 | const firstArg = arguments[0]; 6 | // Use startsWith instead of indexOf as a small optimization for when large strings are logged. 7 | if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ReactDOM.render is no longer supported in React 18.')) { 8 | // Suppress the error. 9 | } else { 10 | oldConsoleError.apply(console, arguments); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /js_src/index.mjs: -------------------------------------------------------------------------------- 1 | import './react.mjs'; 2 | import './react_dom.mjs'; 3 | -------------------------------------------------------------------------------- /js_src/react.mjs: -------------------------------------------------------------------------------- 1 | // Custom dart side methods 2 | import DartHelpers from './_dart_helpers'; 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import createClass from 'create-react-class'; 6 | 7 | globalThis.React = { 8 | ...React, 9 | createClass, // TODO: Remove this once over_react_test doesnt rely on createClass. 10 | PropTypes: { ...PropTypes }, // Only needed to support legacy context until we update. lol jk we need it for prop validation now. 11 | __isDevelopment: process.env.NODE_ENV == 'development', 12 | }; 13 | 14 | Object.assign(globalThis, { 15 | ...DartHelpers, 16 | }); 17 | 18 | if (process.env.NODE_ENV == 'development') { 19 | import('./dart_env_dev.mjs'); 20 | } 21 | 22 | // IE 11 polyfill 23 | // Source: https://github.com/webcomponents/webcomponents-platform/issues/2 24 | if (!('baseURI' in Node.prototype)) { 25 | Object.defineProperty(Node.prototype, 'baseURI', { 26 | get: function () { 27 | const base = (this.ownerDocument || this).querySelector('base'); 28 | return (base || window.location).href; 29 | }, 30 | configurable: true, 31 | enumerable: true, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /js_src/react_dom.mjs: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import { createRoot } from 'react-dom/client'; 3 | import * as ReactRedux from 'react-redux'; 4 | 5 | globalThis.ReactDOM = { 6 | ...ReactDOM, 7 | // Import createRoot from react-dom/client to avoid the warning about ReactDOM.createRoot. 8 | createRoot, 9 | }; 10 | globalThis.ReactRedux = ReactRedux; 11 | 12 | if (process.env.NODE_ENV == 'development') { 13 | import('./react_dom_dev.mjs'); 14 | } 15 | -------------------------------------------------------------------------------- /js_src/react_dom_dev.mjs: -------------------------------------------------------------------------------- 1 | import ShallowRenderer from 'react-test-renderer/shallow'; 2 | import TestUtils from 'react-dom/test-utils'; 3 | 4 | if (!globalThis.React.addons) { 5 | globalThis.React.addons = {}; 6 | } 7 | globalThis.React.addons.TestUtils = TestUtils; 8 | if (!globalThis.React.addons.TestUtils.createRenderer) { 9 | globalThis.React.addons.TestUtils.createRenderer = ShallowRenderer.createRenderer; 10 | } 11 | -------------------------------------------------------------------------------- /lib/react_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2016, the Clean project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // ignore_for_file: deprecated_member_use_from_same_package 6 | library react_client; 7 | 8 | export 'package:react/react.dart' show ReactComponentFactoryProxy, ComponentFactory; 9 | export 'package:react/react_client/react_interop.dart' show ReactElement, ReactJsComponentFactory, inReactDevMode, Ref; 10 | export 'package:react/react_client/component_factory.dart' 11 | show 12 | listifyChildren, 13 | unconvertJsProps, 14 | ReactDomComponentFactoryProxy, 15 | ReactJsComponentFactoryProxy, 16 | ReactDartComponentFactoryProxy, 17 | ReactDartComponentFactoryProxy2, 18 | ReactJsContextComponentFactoryProxy, 19 | ReactDartFunctionComponentFactoryProxy, 20 | JsBackedMapComponentFactoryMixin; 21 | export 'package:react/react_client/zone.dart' show componentZone; 22 | export 'package:react/src/react_client/chain_refs.dart' show chainRefs, chainRefList; 23 | export 'package:react/src/typedefs.dart' show JsFunctionComponent, ReactNode; 24 | 25 | /// Method used to initialize the React environment. 26 | /// 27 | /// > __DEPRECATED.__ 28 | /// > 29 | /// > Environment configuration is now done by default and should not be altered. This can now be removed. 30 | @Deprecated('Calls to this function are no longer required and can be removed.') 31 | void setClientConfiguration() {} 32 | -------------------------------------------------------------------------------- /lib/react_client/bridge.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library react.react_client.bridge; 3 | 4 | import 'package:js/js.dart'; 5 | import 'package:meta/meta.dart'; 6 | import 'package:react/react.dart'; 7 | import 'package:react/react_client/js_backed_map.dart'; 8 | import 'package:react/src/typedefs.dart'; 9 | 10 | /// A function that returns a bridge instance for use with a given [component]. 11 | /// 12 | /// Where possible, it is recommended to avoid creating bridge objects 13 | /// unnecessarily, and instead reuse a const instance. 14 | /// 15 | /// See [Component2BridgeImpl.bridgeFactory] for an example. 16 | typedef Component2BridgeFactory = Component2Bridge Function(Component2 component); 17 | 18 | /// A bridge from the Dart component instance to the JS component instance, 19 | /// encapsulating non-lifecycle-related functionality that is injected into 20 | /// component instances upon creation. 21 | /// 22 | /// Each rendered Dart component has a bridge associated with it, which is 23 | /// accessible via [forComponent]. Most implementations will share a single 24 | /// bridge instance across many components. 25 | /// 26 | /// These implementations translate Dart calls into JS calls, performing any 27 | /// interop necessary. 28 | /// 29 | /// See [Component2BridgeImpl] for the basic implementation. 30 | /// 31 | /// __For internal/advanced use only.__ 32 | abstract class Component2Bridge { 33 | /// Associates a bridge with a component instance. 34 | /// 35 | /// Protected; use [forComponent] instead. 36 | @protected 37 | static final Expando bridgeForComponent = Expando(); 38 | 39 | const Component2Bridge(); 40 | 41 | /// Returns the bridge instance associated with the given [component]. 42 | /// 43 | /// This will only be available for components that are instantiated by the 44 | /// `ReactDartComponentFactoryProxy2`, and not when manually instantiated. 45 | /// 46 | /// __For internal/advanced use only.__ 47 | static Component2Bridge? forComponent(Component2 component) => bridgeForComponent[component]; 48 | 49 | void setState(Component2 component, Map? newState, SetStateCallback? callback); 50 | void setStateWithUpdater(Component2 component, StateUpdaterCallback stateUpdater, SetStateCallback? callback); 51 | void forceUpdate(Component2 component, SetStateCallback? callback); 52 | JsMap jsifyPropTypes( 53 | covariant Component2 component, covariant Map*/ Function> propTypes); 54 | } 55 | 56 | // TODO 3.1.0-wip custom adapter for over_react to avoid typedPropsFactory usages? 57 | 58 | /// A basic bridge implementation compatible with [Component2]. 59 | /// 60 | /// See [Component2Bridge] for more info. 61 | class Component2BridgeImpl extends Component2Bridge { 62 | const Component2BridgeImpl(); 63 | 64 | /// Returns a const bridge instance suitable for use with any component. 65 | /// 66 | /// See [Component2BridgeFactory] for more info. 67 | // ignore: prefer_constructors_over_static_methods 68 | static Component2BridgeImpl bridgeFactory(Component2 _) => const Component2BridgeImpl(); 69 | 70 | @override 71 | void forceUpdate(Component2 component, SetStateCallback? callback) { 72 | if (callback == null) { 73 | component.jsThis.forceUpdate(); 74 | } else { 75 | component.jsThis.forceUpdate(allowInterop(callback)); 76 | } 77 | } 78 | 79 | @override 80 | void setState(Component2 component, Map? newState, SetStateCallback? callback) { 81 | // Short-circuit to match the ReactJS 16 behavior of not re-rendering the component if newState is null. 82 | if (newState == null) return; 83 | 84 | final firstArg = jsBackingMapOrJsCopy(newState); 85 | 86 | if (callback == null) { 87 | component.jsThis.setState(firstArg); 88 | } else { 89 | component.jsThis.setState(firstArg, allowInterop(([_]) { 90 | callback(); 91 | })); 92 | } 93 | } 94 | 95 | @override 96 | void setStateWithUpdater(Component2 component, StateUpdaterCallback stateUpdater, SetStateCallback? callback) { 97 | final firstArg = allowInterop((JsMap jsPrevState, JsMap jsProps, [_]) { 98 | final value = stateUpdater( 99 | JsBackedMap.backedBy(jsPrevState), 100 | JsBackedMap.backedBy(jsProps), 101 | ); 102 | if (value == null) return null; 103 | return jsBackingMapOrJsCopy(value); 104 | }); 105 | 106 | if (callback == null) { 107 | component.jsThis.setState(firstArg); 108 | } else { 109 | component.jsThis.setState(firstArg, allowInterop(([_]) { 110 | callback(); 111 | })); 112 | } 113 | } 114 | 115 | @override 116 | JsMap jsifyPropTypes( 117 | covariant Component2 component, covariant Map*/ Function> propTypes) { 118 | return JsBackedMap.from(propTypes.map((propKey, validator) { 119 | // Wraps the propValidator methods that users provide in order to allow them to be consumable from javascript. 120 | dynamic handlePropValidator( 121 | JsMap props, 122 | String propName, 123 | String? componentName, 124 | String location, 125 | String? propFullName, 126 | // This is a required argument of PropTypes but is usually hidden from the JS consumer. 127 | String? secret, 128 | ) { 129 | // Create a Dart consumable version of the JsMap. 130 | final convertedProps = JsBackedMap.fromJs(props); 131 | // Call the users validator with the newly wrapped props. 132 | final error = validator( 133 | convertedProps, 134 | PropValidatorInfo( 135 | propName: propName, 136 | componentName: componentName, 137 | location: location, 138 | propFullName: propFullName, 139 | )); 140 | return error == null ? null : _JsError(error.toString()); 141 | } 142 | 143 | return MapEntry(propKey, allowInterop(handlePropValidator)); 144 | })).jsObject; 145 | } 146 | } 147 | 148 | @JS('Error') 149 | class _JsError { 150 | external _JsError(message); 151 | } 152 | -------------------------------------------------------------------------------- /lib/react_client/js_backed_map.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library js_map; 3 | 4 | import 'dart:collection'; 5 | import 'dart:core'; 6 | import 'dart:js_util' as js_util; 7 | 8 | import 'package:js/js.dart'; 9 | import 'package:react/src/js_interop_util.dart'; 10 | import 'package:react/src/react_client/private_utils.dart'; 11 | 12 | /// A view into a JavaScript object ([jsObject]) that conforms to the Dart [Map] interface. 13 | /// 14 | /// The keys are all enumerable properties of the [jsObject], though non-enumerable properties may also be accessed. 15 | /// For this reason, it is recommended to use plain, simple JavaScript objects. 16 | /// 17 | /// Keys should be strings, as any other object (primitive or non-primitive), will be coerced by JavaScript 18 | /// to a string upon read/write. This includes `null`, which gets coerced to the string `'null'`. 19 | /// 20 | /// For performance reasons, keys are not validated. 21 | /// 22 | /// Iteration order is arbitrary, and is based on the current browser's implementation. 23 | /// 24 | /// Two JsBackedMap instances are considered equal if they are backed by the same [jsObject]. 25 | class JsBackedMap extends MapBase { 26 | final JsMap jsObject; 27 | 28 | /// Creates a JsBackedMap instance backed by a new [jsObject]. 29 | JsBackedMap() : jsObject = JsMap(); 30 | 31 | /// Creates a JsBackedMap instance backed by [jsObject]. 32 | JsBackedMap.backedBy(this.jsObject); 33 | 34 | /// Creates a JsBackedMap instance that contains all key/value pairs of [other]. 35 | factory JsBackedMap.from(Map other) => JsBackedMap()..addAll(other); 36 | 37 | /// Creates a JsBackedMap instance that contains all key/value pairs of the JS object [jsOther]. 38 | factory JsBackedMap.fromJs(JsMap jsOther) => JsBackedMap()..addAllFromJs(jsOther); 39 | 40 | // Private helpers with narrower typing than we want to expose, for use in other methods 41 | List get _keys => objectKeys(jsObject); 42 | // Use keys to access the value instead oof `Object.values` since MSIE 11 doesn't support it 43 | List get _values => _keys.map((key) => this[key]).toList(); 44 | 45 | /// Adds all key/value pairs of the JS object [jsOther] to this map. 46 | /// 47 | /// If a key of [jsOther] is already in this map, its value is overwritten. 48 | /// 49 | /// The operation is equivalent to doing `this[key] = value` for each key and associated value in [jsOther]. 50 | /// 51 | /// This is similar to [addAll], but for a JsMap instead of a JsBackedMap/Map. 52 | void addAllFromJs(JsMap jsOther) { 53 | _Object.assign(jsObject, jsOther); 54 | } 55 | 56 | // ---------------------------------- 57 | // MapBase implementations 58 | // ---------------------------------- 59 | 60 | @override 61 | dynamic /*?*/ operator [](Object? key) { 62 | return DartValueWrapper.unwrapIfNeeded(js_util.getProperty(jsObject, nonNullableJsPropertyName(key))); 63 | } 64 | 65 | @override 66 | void operator []=(dynamic key, dynamic value) { 67 | js_util.setProperty(jsObject, nonNullableJsPropertyName(key), DartValueWrapper.wrapIfNeeded(value)); 68 | } 69 | 70 | @override 71 | Iterable get keys => _keys; 72 | 73 | @override 74 | dynamic /*?*/ remove(Object? key) { 75 | final value = this[key]; 76 | _Reflect.deleteProperty(jsObject, key); 77 | return value; 78 | } 79 | 80 | @override 81 | void clear() { 82 | for (final key in _keys) { 83 | _Reflect.deleteProperty(jsObject, key); 84 | } 85 | } 86 | 87 | // ---------------------------------- 88 | // Optimized MapBase overrides 89 | // ---------------------------------- 90 | 91 | @override 92 | void addAll(Map other) { 93 | if (other is JsBackedMap) { 94 | // Object.assign is more efficient than iterating through and setting properties in Dart. 95 | addAllFromJs(other.jsObject); 96 | } else { 97 | super.addAll(other); 98 | } 99 | } 100 | 101 | @override 102 | bool containsKey(Object? key) => js_util.hasProperty(jsObject, nonNullableJsPropertyName(key)); 103 | 104 | @override 105 | Iterable get values => _values; 106 | 107 | // todo figure out if this is faster than default implementation 108 | @override 109 | bool containsValue(Object? value) => _values.contains(value); 110 | 111 | @override 112 | bool operator ==(other) => other is JsBackedMap && other.jsObject == jsObject; 113 | 114 | @override 115 | int get hashCode { 116 | // Work around DDC throwing when accessing hashCode on frozen JS objects: 117 | // https://github.com/dart-lang/sdk/issues/36354 118 | try { 119 | return jsObject.hashCode; 120 | } catch (_) {} 121 | 122 | // While constant hashCode like this one are not high-quality and may cause 123 | // hashCode collisions more often, they are completely valid. 124 | // For more information, see the `Object.hashCode` doc comment. 125 | return 0; 126 | } 127 | } 128 | 129 | /// A utility interop class for a plain JS "object"/"map"/"dictionary". 130 | /// 131 | /// This class provides no functionality, and exists solely as a placeholder 132 | /// type for use in [JsBackedMap] and other related utilities. 133 | @JS() 134 | @anonymous 135 | class JsMap { 136 | external factory JsMap(); 137 | } 138 | 139 | /// Returns a JsMap version of [map], which will be either: 140 | /// 141 | /// - the backing JsMap if [map] is a [JsBackedMap] 142 | /// - a JsMap copy of [map] 143 | /// 144 | /// This method is useful when the map needs to be passed to a JS function 145 | /// and you want to avoid copying it when possible. 146 | /// 147 | /// If a copy is always needed, use [JsBackedMap.from] instead. 148 | JsMap jsBackingMapOrJsCopy(Map map) { 149 | // todo is it faster to just always do .from? 150 | if (map is JsBackedMap) { 151 | return map.jsObject; 152 | } else { 153 | return JsBackedMap.from(map).jsObject; 154 | } 155 | } 156 | 157 | @JS('Object') 158 | abstract class _Object { 159 | external static void assign(JsMap to, JsMap from); 160 | } 161 | 162 | @JS('Reflect') 163 | abstract class _Reflect { 164 | external static bool deleteProperty(JsMap target, dynamic propertyKey); 165 | } 166 | -------------------------------------------------------------------------------- /lib/react_client/js_interop_helpers.dart: -------------------------------------------------------------------------------- 1 | /// Utilities for reading/modifying dynamic keys on JavaScript objects 2 | /// and converting Dart [Map]s to JavaScript objects. 3 | @JS() 4 | library react_client.js_interop_helpers; 5 | 6 | import 'dart:collection'; 7 | import 'dart:js_util'; 8 | 9 | import 'package:js/js.dart'; 10 | import 'package:react/react_client/react_interop.dart' show forwardRef2; 11 | import 'package:react/src/js_interop_util.dart'; 12 | 13 | import 'js_backed_map.dart'; 14 | 15 | /// Like [identityHashCode], but uses a different hash for JS objects to work around: 16 | /// - an issue where [identityHashCode] adds an unwanted `$identityHash` property on JS objects: https://github.com/dart-lang/sdk/issues/47595 17 | /// - an issue where [identityHashCode] throws for frozen objects: https://github.com/dart-lang/sdk/issues/36354 18 | int _jsObjectFriendlyIdentityHashCode(Object object) { 19 | // Try detecting JS objects so we don't add properties to them. 20 | // Workaround for https://github.com/dart-lang/sdk/issues/47595 21 | if (object is JsMap) return _anonymousJsObjectOrFrozenObjectHashCode(object); 22 | 23 | // If this fails, then most likely the object is a frozen JS object, such as props object or a variadic JSX children Array. 24 | // Note that props objects are typically handled by the above is JsMap case, though. 25 | // Fall back to a safe implementation. 26 | try { 27 | return identityHashCode(object); 28 | } catch (_) { 29 | return _anonymousJsObjectOrFrozenObjectHashCode(object); 30 | } 31 | } 32 | 33 | /// A hashCode implementation for anonymous JS objects or frozen objects. 34 | /// 35 | /// Even though the current implementation of returning the same hash code for all values is low-quality 36 | /// since all JS objects will collide, it is valid since it always returns the same value for the same object. 37 | /// 38 | /// We also don't expect many JS objects or frozen objects to be passed into [jsifyAndAllowInterop], 39 | /// so the quality of this hash code is not of much concern. 40 | int _anonymousJsObjectOrFrozenObjectHashCode(Object _) => 0; 41 | 42 | // The following code is adapted from `package:js` in the dart-lang/sdk repo: 43 | // https://github.com/dart-lang/sdk/blob/2.2.0/sdk/lib/js_util/dart2js/js_util_dart2js.dart#L27 44 | // 45 | // Copyright 2012, the Dart project authors. All rights reserved. 46 | // Redistribution and use in source and binary forms, with or without 47 | // modification, are permitted provided that the following conditions are 48 | // met: 49 | // * Redistributions of source code must retain the above copyright 50 | // notice, this list of conditions and the following disclaimer. 51 | // * Redistributions in binary form must reproduce the above 52 | // copyright notice, this list of conditions and the following 53 | // disclaimer in the documentation and/or other materials provided 54 | // with the distribution. 55 | // * Neither the name of Google Inc. nor the names of its 56 | // contributors may be used to endorse or promote products derived 57 | // from this software without specific prior written permission. 58 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 59 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 60 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 61 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 62 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 63 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 64 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 65 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 66 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 67 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 68 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 69 | 70 | /// The same as [jsify], except [Function]s are converted with [allowInterop]. 71 | /// 72 | /// --- 73 | /// 74 | /// *From [jsify]'s docs:* 75 | /// 76 | /// WARNING: performance of this method is much worse than other util 77 | /// methods in this library. Only use this method as a last resort. 78 | /// 79 | /// Recursively converts a JSON-like collection of Dart objects to a 80 | /// collection of JavaScript objects and returns a `JsObject` proxy to it. 81 | /// 82 | /// [object] must be a [Map] or [Iterable], the contents of which are also 83 | /// converted. Maps and Iterables are copied to a new JavaScript object. 84 | /// Primitives and other transferable values are directly converted to their 85 | /// JavaScript type, and all other objects are proxied. 86 | dynamic jsifyAndAllowInterop(object) { 87 | if (object is! Map && object is! Iterable) { 88 | throw ArgumentError.value(object, 'object', 'must be a Map or Iterable'); 89 | } 90 | return _convertDataTree(object); 91 | } 92 | 93 | _convertDataTree(data) { 94 | // Use _jsObjectFriendlyIdentityHashCode instead of `identityHashCode`/`Map.identity()` 95 | // to work around https://github.com/dart-lang/sdk/issues/47595 96 | final _convertedObjects = LinkedHashMap(equals: identical, hashCode: _jsObjectFriendlyIdentityHashCode); 97 | 98 | _convert(o) { 99 | if (_convertedObjects.containsKey(o)) { 100 | return _convertedObjects[o]; 101 | } 102 | if (o is Map) { 103 | final convertedMap = newObject() as Object; 104 | _convertedObjects[o] = convertedMap; 105 | for (final key in o.keys) { 106 | setProperty(convertedMap, nonNullableJsPropertyName(key), _convert(o[key])); 107 | } 108 | return convertedMap; 109 | } else if (o is Iterable) { 110 | final convertedList = []; 111 | _convertedObjects[o] = convertedList; 112 | convertedList.addAll(o.map(_convert)); 113 | return convertedList; 114 | } else if (o is Function) { 115 | final convertedFunction = allowInterop(o); 116 | _convertedObjects[o] = convertedFunction; 117 | return convertedFunction; 118 | } else { 119 | return o; 120 | } 121 | } 122 | 123 | return _convert(data); 124 | } 125 | 126 | /// Keeps track of functions found when converting JS props to Dart props. 127 | /// 128 | /// See: [forwardRef2] for usage / context. 129 | final isRawJsFunctionFromProps = Expando(); 130 | -------------------------------------------------------------------------------- /lib/react_client/zone.dart: -------------------------------------------------------------------------------- 1 | library react_client.zone; 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:meta/meta.dart'; 6 | 7 | /// The zone in which React will call component lifecycle methods. 8 | /// 9 | /// This can be used to sync a test's zone and React's component zone, ensuring that component prop callbacks and 10 | /// lifecycle method output all occurs within the same zone as the test. 11 | /// 12 | /// __Example:__ 13 | /// 14 | /// test('zone test', () { 15 | /// componentZone = Zone.current; 16 | /// 17 | /// // ... test your component 18 | /// } 19 | @visibleForTesting 20 | Zone componentZone = Zone.root; 21 | -------------------------------------------------------------------------------- /lib/react_dom.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Clean project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library react_dom; 6 | 7 | // ignore: deprecated_member_use_from_same_package 8 | import 'package:react/react.dart' show Component; 9 | import 'package:react/react_client/react_interop.dart' show ReactDom; 10 | import 'package:react/src/react_client/private_utils.dart' show validateJsApiThenReturn; 11 | 12 | /// Renders a `ReactElement` into the DOM in the supplied `container` and returns a reference to the component 13 | /// (or returns null for stateless components). 14 | /// 15 | /// If the `ReactElement` was previously rendered into the `container`, this will perform an update on it and only 16 | /// mutate the DOM as necessary to reflect the latest React component. 17 | /// 18 | /// TODO: Is there any reason to omit the `ReactElement` type for `component` or the `Element` type for `container`? 19 | Function render = validateJsApiThenReturn(() => ReactDom.render); 20 | 21 | /// Removes a mounted React component from the DOM and cleans up its event handlers and state. 22 | /// 23 | /// > Returns `false` if no component was mounted in the container specified via [render], otherwise returns `true`. 24 | Function unmountComponentAtNode = validateJsApiThenReturn(() => ReactDom.unmountComponentAtNode); 25 | 26 | /// If the component has been mounted into the DOM, this returns the corresponding native browser DOM `Element`. 27 | Function findDOMNode = validateJsApiThenReturn(() => _findDomNode); 28 | 29 | dynamic _findDomNode(component) { 30 | // ignore: deprecated_member_use_from_same_package 31 | return ReactDom.findDOMNode(component is Component ? component.jsThis : component); 32 | } 33 | -------------------------------------------------------------------------------- /lib/react_dom_server.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Clean project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | library react_dom_server; 6 | 7 | /// Renders a ReactElement to its initial HTML. This should only be used on the server. 8 | /// React will return an HTML string. You can use this method to generate HTML on the 9 | /// server and send the markup down on the initial request for faster page loads and to 10 | /// allow search engines to crawl your pages for SEO purposes. 11 | 12 | /// If you call `ReactDOM.render()` on a node that already has this server-rendered markup, 13 | /// React will preserve it and only attach event handlers, allowing you to have a very 14 | /// performant first-load experience. 15 | var renderToString; 16 | 17 | /// Similar to [renderToString], except this doesn't create extra DOM attributes such as 18 | /// data-react-id, that React uses internally. This is useful if you want to use React 19 | /// as a simple static page generator, as stripping away the extra attributes can save 20 | /// lots of bytes. 21 | var renderToStaticMarkup; 22 | 23 | /// Sets configuration based on passed functions. 24 | /// 25 | /// Passes arguments to global variables. 26 | setReactDOMServerConfiguration(customRenderToString, customRenderToStaticMarkup) { 27 | renderToString = customRenderToString; 28 | renderToStaticMarkup = customRenderToStaticMarkup; 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/context.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2016, the Clean project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | @JS() 5 | library react_dart_context; 6 | 7 | import 'dart:js_util'; 8 | 9 | import 'package:js/js.dart'; 10 | import 'package:react/react_client/react_interop.dart'; 11 | import 'package:react/react_client/component_factory.dart' show ReactJsContextComponentFactoryProxy; 12 | import 'package:react/src/react_client/private_utils.dart'; 13 | 14 | /// The return type of [createContext], Wraps [ReactContext] for use in Dart. 15 | /// Allows access to [Provider] and [Consumer] Components. 16 | /// 17 | /// __Should not be instantiated without using [createContext]__ 18 | /// 19 | /// __Example__: 20 | /// 21 | /// Context MyContext = createContext('test'); 22 | /// 23 | /// class MyContextTypeClass extends react.Component2 { 24 | /// @override 25 | /// final contextType = MyContext; 26 | /// 27 | /// render() { 28 | /// return react.span({}, [ 29 | /// '${this.context}', // Outputs: 'test' 30 | /// ]); 31 | /// } 32 | /// } 33 | /// 34 | /// // OR 35 | /// 36 | /// Context MyContext = createContext(); 37 | /// 38 | /// class MyClass extends react.Component2 { 39 | /// render() { 40 | /// return MyContext.Provider({'value': 'new context value'}, [ 41 | /// MyContext.Consumer({}, (value) { 42 | /// return react.span({}, [ 43 | /// '$value', // Outputs: 'new context value' 44 | /// ]), 45 | /// }); 46 | /// ]); 47 | /// } 48 | /// } 49 | /// 50 | /// Learn more at: https://reactjs.org/docs/context.html 51 | class Context { 52 | Context(this.Provider, this.Consumer, this._jsThis); 53 | final ReactContext _jsThis; 54 | 55 | /// Every [Context] object comes with a Provider component that allows consuming components to subscribe 56 | /// to context changes. 57 | /// 58 | /// Accepts a `value` prop to be passed to consuming components that are descendants of this [Provider]. 59 | final ReactJsContextComponentFactoryProxy Provider; 60 | 61 | /// A React component that subscribes to context changes. 62 | /// Requires a function as a child. The function receives the current context value and returns a React node. 63 | final ReactJsContextComponentFactoryProxy Consumer; 64 | ReactContext get jsThis => _jsThis; 65 | } 66 | 67 | /// Creates a [Context] object. When React renders a component that subscribes to this [Context] 68 | /// object it will read the current context value from the closest matching Provider above it in the tree. 69 | /// 70 | /// The `defaultValue` argument is only used when a component does not have a matching [Context.Provider] 71 | /// above it in the tree. This can be helpful for testing components in isolation without wrapping them. 72 | /// 73 | /// __Example__: 74 | /// 75 | /// react.Context MyContext = react.createContext('test'); 76 | /// 77 | /// class MyContextTypeClass extends react.Component2 { 78 | /// @override 79 | /// final contextType = MyContext; 80 | /// 81 | /// render() { 82 | /// return react.span({}, [ 83 | /// '${this.context}', // Outputs: 'test' 84 | /// ]); 85 | /// } 86 | /// } 87 | /// 88 | /// ___ OR ___ 89 | /// 90 | /// react.Context MyContext = react.createContext(); 91 | /// 92 | /// class MyClass extends react.Component2 { 93 | /// render() { 94 | /// return MyContext.Provider({'value': 'new context value'}, [ 95 | /// MyContext.Consumer({}, (value) { 96 | /// return react.span({}, [ 97 | /// '$value', // Outputs: 'new context value' 98 | /// ]), 99 | /// }); 100 | /// ]); 101 | /// } 102 | /// } 103 | /// 104 | /// Learn more: https://reactjs.org/docs/context.html#reactcreatecontext 105 | Context createContext([ 106 | TValue? defaultValue, 107 | int Function(TValue? currentValue, TValue? nextValue)? calculateChangedBits, 108 | ]) { 109 | final jsDefaultValue = ContextHelpers.jsifyNewContext(defaultValue); 110 | 111 | ReactContext JSContext; 112 | if (calculateChangedBits != null) { 113 | int jsifyCalculateChangedBitsArgs(currentValue, nextValue) { 114 | return calculateChangedBits( 115 | ContextHelpers.unjsifyNewContext(currentValue) as TValue?, 116 | ContextHelpers.unjsifyNewContext(nextValue) as TValue?, 117 | ); 118 | } 119 | 120 | JSContext = React.createContext(jsDefaultValue, allowInterop(jsifyCalculateChangedBitsArgs)); 121 | } else { 122 | JSContext = React.createContext(jsDefaultValue); 123 | } 124 | 125 | return Context( 126 | ReactJsContextComponentFactoryProxy(JSContext.Provider, isProvider: true), 127 | ReactJsContextComponentFactoryProxy(JSContext.Consumer, isConsumer: true), 128 | JSContext, 129 | ); 130 | } 131 | 132 | // A JavaScript symbol that we use as the key in a JS Object to wrap the Dart. 133 | @JS() 134 | external Object get _reactDartContextSymbol; 135 | 136 | /// A context utility for assisting with common needs of ReactDartContext. 137 | /// 138 | /// __For internal/advanced use only.__ 139 | abstract class ContextHelpers { 140 | // Wraps context value in a JS Object for use on the JS side. 141 | // It is wrapped so that the same Dart value can be retrieved from Dart with [_unjsifyNewContext]. 142 | static dynamic jsifyNewContext(Object? context) { 143 | final jsContextHolder = newObject() as Object; 144 | setProperty(jsContextHolder, _reactDartContextSymbol, DartValueWrapper.wrapIfNeeded(context)); 145 | return jsContextHolder; 146 | } 147 | 148 | // Unwraps context value from a JS Object for use on the Dart side. 149 | // The value is unwrapped so that the same Dart value can be passed through js and retrived by Dart 150 | // when used with [_jsifyNewContext]. 151 | static dynamic unjsifyNewContext(Object? interopContext) { 152 | if (interopContext != null && hasProperty(interopContext, _reactDartContextSymbol)) { 153 | return DartValueWrapper.unwrapIfNeeded(getProperty(interopContext, _reactDartContextSymbol)); 154 | } 155 | return interopContext; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/src/js_interop_util.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library js_interop_util; 3 | 4 | import 'dart:js_util'; 5 | 6 | import 'package:js/js.dart'; 7 | 8 | @JS('Object.keys') 9 | // We can't type this as List due to https://github.com/dart-lang/sdk/issues/37676 10 | external List objectKeys(Object object); 11 | 12 | @JS() 13 | @anonymous 14 | class JsPropertyDescriptor { 15 | external factory JsPropertyDescriptor({dynamic value}); 16 | } 17 | 18 | @JS('Object.defineProperty') 19 | external void defineProperty(dynamic object, String propertyName, JsPropertyDescriptor descriptor); 20 | 21 | String? getJsFunctionName(Function object) => 22 | (getProperty(object, 'name') ?? getProperty(object, '\$static_name')) as String?; 23 | 24 | /// Creates JS `Promise` which is resolved when [future] completes. 25 | /// 26 | /// See also: 27 | /// - [promiseToFuture] 28 | Promise futureToPromise(Future future) { 29 | return Promise(allowInterop((resolve, reject) { 30 | future.then((result) => resolve(result), onError: reject); 31 | })); 32 | } 33 | 34 | @JS() 35 | abstract class Promise { 36 | external factory Promise( 37 | Function(dynamic Function(dynamic value) resolve, dynamic Function(dynamic error) reject) executor); 38 | 39 | external Promise then(dynamic Function(dynamic value) onFulfilled, [dynamic Function(dynamic error) onRejected]); 40 | } 41 | 42 | /// Converts an arbitrary [key] into a value that can be passed into `dart:js_util` methods 43 | /// such as [getProperty] and [setProperty], which only accept non-nullable 44 | /// `name` arguments. 45 | /// 46 | /// This function converts `null` to the string `'null'`, which is what JavaScript does under 47 | /// the hood when `null` is used as an object property. Source: the ES6 spec: 48 | /// - https://262.ecma-international.org/6.0/#sec-property-accessors-runtime-semantics-evaluation 49 | /// - https://262.ecma-international.org/6.0/#sec-topropertykey 50 | /// 51 | /// See also: https://github.com/dart-lang/sdk/issues/45219 52 | Object nonNullableJsPropertyName(Object? key) => key ?? 'null'; 53 | -------------------------------------------------------------------------------- /lib/src/prop_validator.dart: -------------------------------------------------------------------------------- 1 | /// Typedef for `Component2.propTypes` - used to check the validity of one or more of the [props]. 2 | /// 3 | /// [info] is a [PropValidatorInfo] class that contains metadata about the prop referenced as 4 | /// the key within the `Component2.propTypes` map. 5 | /// `propName`, `componentName`, `location` and `propFullName` are available. 6 | typedef PropValidator = Error? Function(T props, PropValidatorInfo info); 7 | 8 | /// Metadata about a prop being validated by a [PropValidator]. 9 | class PropValidatorInfo { 10 | final String propName; 11 | final String? componentName; 12 | final String location; 13 | final String? propFullName; 14 | 15 | const PropValidatorInfo({ 16 | required this.propName, 17 | required this.componentName, 18 | required this.location, 19 | required this.propFullName, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/react_client/chain_refs.dart: -------------------------------------------------------------------------------- 1 | import 'package:js/js_util.dart'; 2 | import 'package:react/react.dart' as react; 3 | import 'package:react/react_client/react_interop.dart'; 4 | import 'package:react/src/react_client/factory_util.dart'; 5 | 6 | /// Returns a ref that updates both [ref1] and [ref2], effectively 7 | /// allowing you to set multiple refs. 8 | /// 9 | /// Useful when you're using ref forwarding and want to also set your own ref. 10 | /// 11 | /// For more information on this problem, see https://github.com/facebook/react/issues/13029. 12 | /// 13 | /// Inputs can be callback refs, [createRef]-based refs ([Ref]), 14 | /// or raw JS `createRef` refs ([JsRef]). 15 | /// 16 | /// If either [ref1] or [ref2] is null, the other ref will be passed through. 17 | /// 18 | /// If both are null, null is returned. 19 | /// 20 | /// ```dart 21 | /// final Example = forwardRef((props, ref) { 22 | /// final localRef = useRef(); 23 | /// return Foo({ 24 | /// ...props, 25 | /// 'ref': chainRefs(localRef, ref), 26 | /// }); 27 | /// }); 28 | /// ``` 29 | dynamic chainRefs(dynamic ref1, dynamic ref2) { 30 | return chainRefList([ref1, ref2]); 31 | } 32 | 33 | /// Like [chainRefs], but takes in a list of [refs]. 34 | dynamic chainRefList(List refs) { 35 | final nonNullRefs = refs.where((ref) => ref != null).toList(growable: false); 36 | 37 | // Wrap in an assert so iteration doesn't take place unnecessarily 38 | assert(() { 39 | nonNullRefs.forEach(_validateChainRefsArg); 40 | return true; 41 | }()); 42 | 43 | // Return null if there are no refs to chain 44 | if (nonNullRefs.isEmpty) return null; 45 | 46 | // Pass through the ref if there's nothing to chain it with 47 | if (nonNullRefs.length == 1) return nonNullRefs[0]; 48 | 49 | // Adapted from https://github.com/smooth-code/react-merge-refs/tree/v1.0.0 50 | // 51 | // Copyright 2019 Smooth Code 52 | // 53 | // Permission is hereby granted, free of charge, to any person obtaining a 54 | // copy of this software and associated documentation files (the 'Software'), 55 | // to deal in the Software without restriction, including without limitation 56 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 57 | // and/or sell copies of the Software, and to permit persons to whom the 58 | // Software is furnished to do so, subject to the following conditions: 59 | // 60 | // The above copyright notice and this permission notice shall be included in 61 | // all copies or substantial portions of the Software. 62 | // 63 | // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 64 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 65 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 66 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 67 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 68 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 69 | // DEALINGS IN THE SOFTWARE. 70 | void _chainedRef(value) { 71 | for (final ref in nonNullRefs) { 72 | if (ref is Function) { 73 | ref(value); 74 | } else if (ref is Ref) { 75 | // ignore: invalid_use_of_protected_member 76 | ref.current = value; 77 | } else { 78 | // ignore: invalid_use_of_protected_member, deprecated_member_use_from_same_package 79 | (ref as JsRef).current = value is react.Component ? value.jsThis : value; 80 | } 81 | } 82 | } 83 | 84 | return _chainedRef; 85 | } 86 | 87 | void _validateChainRefsArg(dynamic ref) { 88 | if (ref is Function(Never)) { 89 | if (isRefArgumentDefinitelyNonNullable(ref)) { 90 | throw AssertionError(nonNullableCallbackRefArgMessage); 91 | } 92 | return; 93 | } 94 | 95 | // Need to duck-type since `is JsRef` will return true for most JS objects. 96 | if (ref is Ref || (ref is JsRef && hasProperty(ref, 'current'))) { 97 | return; 98 | } 99 | 100 | if (ref is String) throw AssertionError('String refs cannot be chained'); 101 | if (ref is Function) throw AssertionError('callback refs must take a single argument'); 102 | 103 | throw AssertionError('Invalid ref type: $ref'); 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/react_client/dart2_interop_workaround_bindings.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library react_client.src.dart2_interop_workaround_bindings; 3 | 4 | import 'dart:html'; 5 | 6 | import 'package:js/js.dart'; 7 | import 'package:react/react_client/react_interop.dart'; 8 | import 'package:react/src/typedefs.dart'; 9 | 10 | @JS() 11 | abstract class ReactDOM { 12 | external static Element? findDOMNode(ReactNode object); 13 | external static dynamic render(ReactNode component, Element element); 14 | external static bool unmountComponentAtNode(Element element); 15 | external static ReactPortal createPortal(ReactNode children, Element container); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/react_client/event_prop_key_to_event_factory.dart: -------------------------------------------------------------------------------- 1 | /// Keys for React DOM event handlers. 2 | final Set knownEventKeys = (() { 3 | final _knownEventKeys = { 4 | // SyntheticClipboardEvent 5 | 'onCopy', 6 | 'onCut', 7 | 'onPaste', 8 | 9 | // SyntheticKeyboardEvent 10 | 'onKeyDown', 11 | 'onKeyPress', 12 | 'onKeyUp', 13 | 14 | // SyntheticCompositionEvent 15 | 'onCompositionStart', 16 | 'onCompositionUpdate', 17 | 'onCompositionEnd', 18 | 19 | // SyntheticFocusEvent 20 | 'onFocus', 21 | 'onBlur', 22 | 23 | // SyntheticFormEvent 24 | 'onChange', 25 | 'onInput', 26 | 'onSubmit', 27 | 'onReset', 28 | 29 | // SyntheticMouseEvent 30 | 'onClick', 31 | 'onContextMenu', 32 | 'onDoubleClick', 33 | 'onDrag', 34 | 'onDragEnd', 35 | 'onDragEnter', 36 | 'onDragExit', 37 | 'onDragLeave', 38 | 'onDragOver', 39 | 'onDragStart', 40 | 'onDrop', 41 | 'onMouseDown', 42 | 'onMouseEnter', 43 | 'onMouseLeave', 44 | 'onMouseMove', 45 | 'onMouseOut', 46 | 'onMouseOver', 47 | 'onMouseUp', 48 | 49 | // SyntheticPointerEvent 50 | 'onGotPointerCapture', 51 | 'onLostPointerCapture', 52 | 'onPointerCancel', 53 | 'onPointerDown', 54 | 'onPointerEnter', 55 | 'onPointerLeave', 56 | 'onPointerMove', 57 | 'onPointerOver', 58 | 'onPointerOut', 59 | 'onPointerUp', 60 | 61 | // SyntheticTouchEvent 62 | 'onTouchCancel', 63 | 'onTouchEnd', 64 | 'onTouchMove', 65 | 'onTouchStart', 66 | 67 | // SyntheticTransitionEvent 68 | 'onTransitionEnd', 69 | 70 | // SyntheticAnimationEvent 71 | 'onAnimationEnd', 72 | 'onAnimationIteration', 73 | 'onAnimationStart', 74 | 75 | // SyntheticUIEvent 76 | 'onScroll', 77 | 78 | // SyntheticWheelEvent 79 | 'onWheel', 80 | }; 81 | 82 | // Add support for capturing variants; e.g., onClick/onClickCapture 83 | for (final key in _knownEventKeys.toList()) { 84 | _knownEventKeys.add(key + 'Capture'); 85 | } 86 | 87 | return _knownEventKeys; 88 | })(); 89 | -------------------------------------------------------------------------------- /lib/src/react_client/factory_util.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library react_client_factory_utils; 3 | 4 | import 'package:js/js.dart'; 5 | 6 | import 'package:react/react_client/component_factory.dart'; 7 | import 'package:react/react_client/js_backed_map.dart'; 8 | import 'package:react/react_client/js_interop_helpers.dart'; 9 | import 'package:react/react_client/react_interop.dart'; 10 | import 'package:react/src/react_client/internal_react_interop.dart'; 11 | import 'package:react/src/typedefs.dart'; 12 | 13 | /// Converts a list of variadic children arguments to children that should be passed to ReactJS. 14 | /// 15 | /// Returns: 16 | /// 17 | /// - `null` if there are no args 18 | /// - the single child if only one was specified 19 | /// - otherwise, the same list of args, will all top-level children validated 20 | ReactNode convertArgsToChildren(List childrenArgs) { 21 | if (childrenArgs.isEmpty) { 22 | return null; 23 | } else if (childrenArgs.length == 1) { 24 | return childrenArgs.single; 25 | } else { 26 | markChildrenValidated(childrenArgs); 27 | return childrenArgs; 28 | } 29 | } 30 | 31 | void convertRefValue(Map args) { 32 | final ref = args['ref']; 33 | if (ref is Ref) { 34 | args['ref'] = ref.jsRef; 35 | } 36 | } 37 | 38 | void convertRefValue2( 39 | Map args, { 40 | bool convertCallbackRefValue = true, 41 | List additionalRefPropKeys = const [], 42 | }) { 43 | final refKeys = ['ref', ...additionalRefPropKeys]; 44 | 45 | for (final refKey in refKeys) { 46 | final ref = args[refKey]; 47 | if (ref is Ref) { 48 | args[refKey] = ref.jsRef; 49 | // If the ref is a callback, pass ReactJS a function that will call it 50 | // with the Dart Component instance, not the ReactComponent instance. 51 | // 52 | // Use _CallbackRef to check arity, since parameters could be non-dynamic, and thus 53 | // would fail the `is _CallbackRef` check. 54 | // See https://github.com/dart-lang/sdk/issues/34593 for more information on arity checks. 55 | // ignore: prefer_void_to_null 56 | } else if (ref is Function(Never) && convertCallbackRefValue) { 57 | assert(!isRefArgumentDefinitelyNonNullable(ref), nonNullableCallbackRefArgMessage); 58 | args[refKey] = allowInterop((dynamic instance) { 59 | // Call as dynamic to perform dynamic dispatch, since we can't cast to _CallbackRef, 60 | // and since calling with non-null values will fail at runtime due to the _CallbackRef typing. 61 | if (instance is ReactComponent && instance.dartComponent != null) { 62 | return (ref as dynamic)(instance.dartComponent); 63 | } 64 | 65 | return (ref as dynamic)(instance); 66 | }); 67 | } 68 | } 69 | } 70 | 71 | const nonNullableCallbackRefArgMessage = 72 | 'Arguments to callback ref functions must not be non-nullable, since React passes null to callback refs' 73 | ' when they\'re detached. For instance, the callback will be called with `null` when the component is unmounted,' 74 | ' and also when the owner component rerenders when using a non-memoized callback ref.' 75 | '\n\nInstead of: `(MyComponent ref) { myComponentRef = ref; }`' 76 | '\nDo either:' 77 | '\n- `(MyComponent? ref) { myComponentRef = ref; }`' 78 | '\n- `(ref) { myComponentRef = ref as MyComponent?; }'; 79 | 80 | /// Returns whether [props] contains a ref that is a callback ref with a non-nullable argument. 81 | bool mapHasCallbackRefWithDefinitelyNonNullableArgument(Map props) { 82 | final ref = props['ref']; 83 | return ref is Function(Never) && isRefArgumentDefinitelyNonNullable(ref); 84 | } 85 | 86 | /// Returns whether the argument to [callbackRef] is definitely a non-nullable type. 87 | /// 88 | /// "Definitely", since function will always return `false` when running under unsound null safety, 89 | /// even if the ref argument is typed as non-nullable. 90 | bool isRefArgumentDefinitelyNonNullable(Function(Never) callbackRef) { 91 | return callbackRef is! Function(Null); 92 | } 93 | 94 | /// Converts a list of variadic children arguments to children that should be passed to ReactJS. 95 | /// 96 | /// Returns: 97 | /// 98 | /// - `null` if there are no args and [shouldAlwaysBeList] is false 99 | /// - `[]` if there are no args and [shouldAlwaysBeList] is true 100 | /// - the single child if only one was specified 101 | /// - otherwise, the same list of args, will all top-level children validated 102 | ReactNode generateChildren(List childrenArgs, {bool shouldAlwaysBeList = false}) { 103 | var children; 104 | 105 | if (childrenArgs.isEmpty) { 106 | if (!shouldAlwaysBeList) return jsUndefined; 107 | children = childrenArgs; 108 | } else if (childrenArgs.length == 1) { 109 | if (shouldAlwaysBeList) { 110 | final singleChild = listifyChildren(childrenArgs.single); 111 | if (singleChild is List) { 112 | children = singleChild; 113 | } 114 | } else { 115 | children = childrenArgs.single; 116 | } 117 | } 118 | 119 | if (children is Iterable && children is! List) { 120 | children = children.toList(growable: false); 121 | } 122 | 123 | if (children == null) { 124 | children = shouldAlwaysBeList ? childrenArgs.map(listifyChildren).toList() : childrenArgs; 125 | markChildrenValidated(children); 126 | } 127 | 128 | return children; 129 | } 130 | 131 | /// Converts [props] into a [JsMap] that can be utilized with `React.createElement()`. 132 | JsMap generateJsProps(Map props, 133 | {bool convertRefValue = true, 134 | bool convertCallbackRefValue = true, 135 | List additionalRefPropKeys = const [], 136 | bool wrapWithJsify = true}) { 137 | final propsForJs = JsBackedMap.from(props); 138 | if (convertRefValue) { 139 | convertRefValue2(propsForJs, 140 | convertCallbackRefValue: convertCallbackRefValue, additionalRefPropKeys: additionalRefPropKeys); 141 | } 142 | 143 | assert(!mapHasCallbackRefWithDefinitelyNonNullableArgument(propsForJs), nonNullableCallbackRefArgMessage); 144 | 145 | return wrapWithJsify ? jsifyAndAllowInterop(propsForJs) as JsMap : propsForJs.jsObject; 146 | } 147 | -------------------------------------------------------------------------------- /lib/src/react_client/lazy.dart: -------------------------------------------------------------------------------- 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 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 | } 82 | -------------------------------------------------------------------------------- /lib/src/react_client/private_utils.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | library react_client_private_utils; 3 | 4 | import 'dart:js_util'; 5 | 6 | import 'package:js/js.dart'; 7 | import 'package:react/react.dart' show Component2; 8 | import 'package:react/react_client/react_interop.dart'; 9 | import 'package:react/src/js_interop_util.dart'; 10 | import 'package:react/src/react_client/component_registration.dart' show registerComponent2; 11 | import 'package:react/src/react_client/internal_react_interop.dart'; 12 | 13 | /// A flag used to cache whether React is accessible. 14 | /// 15 | /// This is used when setting environment variables to ensure they can be set properly. 16 | bool _isJsApiValid = false; 17 | 18 | @Deprecated( 19 | 'This is only used with the legacy context APIs in the deprecated Component, and will be removed along with them.') 20 | InteropContextValue jsifyContext(Map context) { 21 | final interopContext = InteropContextValue(); 22 | context.forEach((key, value) { 23 | // ignore: argument_type_not_assignable 24 | setProperty(interopContext, key, ReactDartContextInternal(value)); 25 | }); 26 | 27 | return interopContext; 28 | } 29 | 30 | /// Validates that React is available (see [validateJsApi]) before returning the result of [computeReturn]). 31 | T validateJsApiThenReturn(T Function() computeReturn) { 32 | validateJsApi(); 33 | return computeReturn(); 34 | } 35 | 36 | @Deprecated( 37 | 'This is only used with the legacy context APIs in the deprecated Component, and will be removed along with them.') 38 | Map unjsifyContext(InteropContextValue interopContext) { 39 | // TODO consider using `contextKeys` for this if perf of objectKeys is bad. 40 | return { 41 | for (final key in objectKeys(interopContext).cast()) 42 | key: (getProperty(interopContext, key) as ReactDartContextInternal?)?.value 43 | }; 44 | } 45 | 46 | /// Validates that React JS has been loaded and its APIs made available on the window, 47 | /// throwing an exception otherwise. 48 | void validateJsApi() { 49 | if (_isJsApiValid) return; 50 | 51 | try { 52 | // Attempt to invoke JS interop methods, which will throw if the 53 | // corresponding JS functions are not available. 54 | React.isValidElement(null); 55 | ReactDom.findDOMNode(null); 56 | // This indirectly calls createReactDartComponentClass2 57 | registerComponent2(() => _DummyComponent2()); 58 | _isJsApiValid = true; 59 | } on NoSuchMethodError catch (_) { 60 | throw Exception('`packages/react/js/react.dev.js` or `packages/react/js/react.min.js` must be loaded.'); 61 | } catch (_) { 62 | throw Exception('Loaded react.js must include react-dart JS interop helpers.'); 63 | } 64 | } 65 | 66 | class _DummyComponent2 extends Component2 { 67 | @override 68 | render() => null; 69 | } 70 | 71 | /// A wrapper around a value that can't be stored in its raw form 72 | /// within a JS object (e.g., a Dart function). 73 | class DartValueWrapper { 74 | final Object? value; 75 | 76 | const DartValueWrapper(this.value); 77 | 78 | static dynamic wrapIfNeeded(Object? value) { 79 | if (value is Function && !identical(allowInterop(value), value)) { 80 | return DartValueWrapper(value); 81 | } 82 | return value; 83 | } 84 | 85 | static dynamic unwrapIfNeeded(Object? value) { 86 | if (value is DartValueWrapper) { 87 | return value.value; 88 | } 89 | return value; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/react_client/synthetic_data_transfer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:react/src/react_client/synthetic_event_wrappers.dart' as events; 4 | 5 | /// Used to wrap `SyntheticMouseEvent.dataTransfer`'s interop value in order to avoid known errors. 6 | /// 7 | /// See for more context. 8 | /// 9 | /// Related: [syntheticDataTransferFactory] 10 | class SyntheticDataTransfer { 11 | /// Gets the type of drag-and-drop operation currently selected or sets the operation to a new type. 12 | /// 13 | /// The value must be `none`, `copy`, `link` or `move`. 14 | /// 15 | /// See 16 | final String? dropEffect; 17 | 18 | /// Provides all of the types of operations that are possible. 19 | /// 20 | /// Must be one of `none`, `copy`, `copyLink`, `copyMove`, `link`, `linkMove`, `move`, `all` or `uninitialized`. 21 | /// 22 | /// See 23 | final String? effectAllowed; 24 | 25 | /// Contains a list of all the local files available on the data transfer. 26 | /// 27 | /// If the drag operation doesn't involve dragging files, this property is an empty list. 28 | /// 29 | /// See 30 | final List files; 31 | 32 | /// Gives the formats that were set in the `dragstart` event. 33 | /// 34 | /// See 35 | final List types; 36 | 37 | SyntheticDataTransfer(this.dropEffect, this.effectAllowed, this.files, this.types); 38 | } 39 | 40 | /// Wrapper for [SyntheticDataTransfer]. 41 | /// 42 | /// [dt] is typed as Object instead of [dynamic] to avoid dynamic calls in the method body, 43 | /// ensuring the code is statically sound. 44 | SyntheticDataTransfer? syntheticDataTransferFactory(Object? dt) { 45 | if (dt == null) return null; 46 | 47 | // `SyntheticDataTransfer` is possible because `createSyntheticMouseEvent` can take in an event that already 48 | // exists and save its `dataTransfer` property to the new event object. When that happens, `dataTransfer` is 49 | // already a `SyntheticDataTransfer` event and should just be returned. 50 | if (dt is SyntheticDataTransfer) return dt; 51 | 52 | List? rawFiles; 53 | List? rawTypes; 54 | 55 | String? effectAllowed; 56 | String? dropEffect; 57 | 58 | // Handle `dt` being either a native DOM DataTransfer object or a JS object that looks like it (events.NonNativeDataTransfer). 59 | // Casting a JS object to DataTransfer fails intermittently in dart2js, and vice-versa fails intermittently in either DDC or dart2js. 60 | // TODO figure out when NonNativeDataTransfer is used. 61 | // 62 | // Some logic here is duplicated to ensure statically-sound access of same-named members. 63 | if (dt is DataTransfer) { 64 | rawFiles = dt.files; 65 | rawTypes = dt.types; 66 | 67 | try { 68 | // Works around a bug in IE where dragging from outside the browser fails. 69 | // Trying to access this property throws the error "Unexpected call to method or property access.". 70 | effectAllowed = dt.effectAllowed; 71 | } catch (_) { 72 | effectAllowed = 'uninitialized'; 73 | } 74 | try { 75 | // For certain types of drag events in IE (anything but ondragenter, ondragover, and ondrop), this fails. 76 | // Trying to access this property throws the error "Unexpected call to method or property access.". 77 | dropEffect = dt.dropEffect; 78 | } catch (_) { 79 | dropEffect = 'none'; 80 | } 81 | } else { 82 | // Assume it's a NonNativeDataTransfer otherwise. 83 | // Perform a cast inside `else` instead of an `else if (dt is ...)` since is-checks for 84 | // anonymous JS objects have undefined behavior. 85 | final castedDt = dt as events.NonNativeDataTransfer; 86 | 87 | rawFiles = castedDt.files; 88 | rawTypes = castedDt.types; 89 | 90 | try { 91 | // Works around a bug in IE where dragging from outside the browser fails. 92 | // Trying to access this property throws the error "Unexpected call to method or property access.". 93 | effectAllowed = castedDt.effectAllowed; 94 | } catch (_) { 95 | effectAllowed = 'uninitialized'; 96 | } 97 | try { 98 | // For certain types of drag events in IE (anything but ondragenter, ondragover, and ondrop), this fails. 99 | // Trying to access this property throws the error "Unexpected call to method or property access.". 100 | dropEffect = castedDt.dropEffect; 101 | } catch (_) { 102 | dropEffect = 'none'; 103 | } 104 | } 105 | 106 | // Copy these lists and ensure they're typed properly. 107 | final files = List.from(rawFiles ?? []); 108 | final types = List.from(rawTypes ?? []); 109 | 110 | return SyntheticDataTransfer(dropEffect, effectAllowed, files, types); 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/react_test_utils/simulate_wrappers.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Clean project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | @JS() 6 | library react.test_utils.simulate_wrappers; 7 | 8 | import 'package:js/js.dart'; 9 | 10 | @JS('React.addons.TestUtils.Simulate') 11 | abstract class Simulate { 12 | external static void animationEnd(componentOrNode, [eventData]); 13 | external static void animationIteration(componentOrNode, [eventData]); 14 | external static void animationStart(componentOrNode, [eventData]); 15 | external static void blur(componentOrNode, [eventData]); 16 | external static void change(componentOrNode, [eventData]); 17 | external static void click(componentOrNode, [eventData]); 18 | external static void contextMenu(componentOrNode, [eventData]); 19 | external static void copy(componentOrNode, [eventData]); 20 | external static void compositionEnd(componentOrNode, [eventData]); 21 | external static void compositionStart(componentOrNode, [eventData]); 22 | external static void compositionUpdate(componentOrNode, [eventData]); 23 | external static void cut(componentOrNode, [eventData]); 24 | external static void doubleClick(componentOrNode, [eventData]); 25 | external static void drag(componentOrNode, [eventData]); 26 | external static void dragEnd(componentOrNode, [eventData]); 27 | external static void dragEnter(componentOrNode, [eventData]); 28 | external static void dragExit(componentOrNode, [eventData]); 29 | external static void dragLeave(componentOrNode, [eventData]); 30 | external static void dragOver(componentOrNode, [eventData]); 31 | external static void dragStart(componentOrNode, [eventData]); 32 | external static void drop(componentOrNode, [eventData]); 33 | external static void focus(componentOrNode, [eventData]); 34 | external static void gotPointerCapture(componentOrNode, [eventData]); 35 | external static void input(componentOrNode, [eventData]); 36 | external static void keyDown(componentOrNode, [eventData]); 37 | external static void keyPress(componentOrNode, [eventData]); 38 | external static void keyUp(componentOrNode, [eventData]); 39 | external static void lostPointerCapture(componentOrNode, [eventData]); 40 | external static void mouseDown(componentOrNode, [eventData]); 41 | external static void mouseMove(componentOrNode, [eventData]); 42 | external static void mouseOut(componentOrNode, [eventData]); 43 | external static void mouseOver(componentOrNode, [eventData]); 44 | external static void mouseUp(componentOrNode, [eventData]); 45 | external static void pointerCancel(componentOrNode, [eventData]); 46 | external static void pointerDown(componentOrNode, [eventData]); 47 | external static void pointerEnter(componentOrNode, [eventData]); 48 | external static void pointerLeave(componentOrNode, [eventData]); 49 | external static void pointerMove(componentOrNode, [eventData]); 50 | external static void pointerOut(componentOrNode, [eventData]); 51 | external static void pointerOver(componentOrNode, [eventData]); 52 | external static void pointerUp(componentOrNode, [eventData]); 53 | external static void paste(componentOrNode, [eventData]); 54 | external static void scroll(componentOrNode, [eventData]); 55 | external static void submit(componentOrNode, [eventData]); 56 | external static void touchCancel(componentOrNode, [eventData]); 57 | external static void touchEnd(componentOrNode, [eventData]); 58 | external static void touchMove(componentOrNode, [eventData]); 59 | external static void touchStart(componentOrNode, [eventData]); 60 | external static void transitionEnd(componentOrNode, [eventData]); 61 | external static void wheel(componentOrNode, [eventData]); 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/typedefs.dart: -------------------------------------------------------------------------------- 1 | library react.typedefs; 2 | 3 | import 'package:react/react.dart'; 4 | import 'package:react/react_client/js_backed_map.dart'; 5 | 6 | /// The type of `Component.ref` specified as a callback. 7 | /// 8 | /// See: 9 | typedef CallbackRef = Function(T? componentOrDomNode); 10 | 11 | /// The function signature for ReactJS Function Components. 12 | /// 13 | /// - [props] will always be supplied as the first argument 14 | /// - [legacyContext] has been deprecated and should not be used but remains for backward compatibility and is necessary 15 | /// to match Dart's generated call signature based on the number of args React provides. 16 | typedef JsFunctionComponent = /*ReactNode*/ dynamic Function(JsMap props, [JsMap? legacyContext]); 17 | 18 | typedef JsForwardRefFunctionComponent = /*ReactNode*/ dynamic Function(JsMap props, dynamic ref); 19 | 20 | /// Typedef for `react.Component.ref`, which should return one of the following specified by the provided [ref]: 21 | /// 22 | /// * `react.Component` if it is a Dart component. 23 | /// * `Element` _(DOM node)_ if it is a React DOM component. 24 | typedef RefMethod = dynamic Function(String ref); 25 | 26 | /// Typedef for the `updater` argument of [Component2.setStateWithUpdater]. 27 | /// 28 | /// See: 29 | typedef StateUpdaterCallback = Map? Function(Map prevState, Map props); 30 | 31 | /// Typedef of a non-transactional [Component2.setState] callback. 32 | /// 33 | /// See: 34 | typedef SetStateCallback = Function(); 35 | 36 | /// A value that can be returned from a component's `render`, or used as `children`. 37 | /// 38 | /// Possible values include: 39 | /// * A `ReactElement` such as a DOM element created using `react.div({})`, or a user-defined component. 40 | /// * A `ReactPortal` created by `createPortal`. 41 | /// * A `String` or `num` (Rendered as text nodes in the DOM). 42 | /// * Booleans or `null` (Render nothing). 43 | /// * A list of, or a `ReactFragment` containing, any/all of the above. 44 | /// 45 | /// See also: https://github.com/facebook/react/blob/b3003047101b4c7a643788a8faf576f7e370fb45/packages/shared/ReactTypes.js#L10 46 | typedef ReactNode = Object?; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dart", 3 | "scripts": { 4 | "build": "pnpm build:dev && pnpm build:prod", 5 | "build:dev": "NODE_ENV=development pnpm vite build", 6 | "build:prod": "NODE_ENV=production pnpm vite build" 7 | }, 8 | "dependencies": { 9 | "vite": "^6.2.3", 10 | "@vitejs/plugin-react": "^4.3.4", 11 | "create-react-class": "^15.7.0", 12 | "prop-types": "^15.7.2", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-redux": "7.2.2", 16 | "react-test-renderer": "^18.0.0", 17 | "redux": "^4.1.2" 18 | }, 19 | "pnpm": { 20 | "onlyBuiltDependencies": [ 21 | "core-js", 22 | "esbuild", 23 | "fsevents" 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: react 2 | version: 7.3.0 3 | description: Bindings of the ReactJS library for building interactive interfaces. 4 | homepage: https://github.com/cleandart/react-dart 5 | environment: 6 | sdk: '>=2.14.0 <4.0.0' 7 | dependencies: 8 | js: ^0.6.3 9 | meta: ^1.16.0 10 | dev_dependencies: 11 | args: ^2.0.0 12 | build_runner: ^2.1.2 13 | build_test: ^2.1.3 14 | build_web_compilers: '>=3.0.0 <5.0.0' 15 | dart_dev: ^4.0.0 16 | dependency_validator: ^3.2.2 17 | glob: ^2.0.0 18 | matcher: ^0.12.11 19 | mocktail: ^1.0.3 20 | test: ^1.17.12 21 | workiva_analysis_options: ^1.3.0 22 | test_html_builder: ^3.0.12 23 | -------------------------------------------------------------------------------- /test/ReactCompositeTestComponent.js: -------------------------------------------------------------------------------- 1 | function compositeComponent() { 2 | return class ReactCompositeTestComponent extends React.Component { 3 | render(){ 4 | return React.createElement("div", {}, 'test js component'); 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/ReactSetStateTestComponent.js: -------------------------------------------------------------------------------- 1 | function getUpdatingSetStateLifeCycleCalls() { 2 | return _updatingSetStateLifeCycleCalls; 3 | } 4 | 5 | var _updatingSetStateLifeCycleCalls = []; 6 | 7 | function getNonUpdatingSetStateLifeCycleCalls() { 8 | return _nonUpdatingSetStateLifeCycleCalls; 9 | } 10 | 11 | var _nonUpdatingSetStateLifeCycleCalls = []; 12 | 13 | function getLatestJSCounter() { 14 | return _counter; 15 | } 16 | 17 | function getUpdatingRenderedCounter() { 18 | return ReactDOM.findDOMNode(updatingInstance).textContent; 19 | } 20 | 21 | function getNonUpdatingRenderedCounter() { 22 | return ReactDOM.findDOMNode(nonUpdatingInstance).textContent; 23 | } 24 | 25 | var _counter; 26 | 27 | class ReactSetStateTestComponent extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | _counter = 1; 31 | this.state = {counter: _counter}; 32 | } 33 | 34 | recordStateChange(newCount) { 35 | _counter = newCount; 36 | } 37 | 38 | recordLifecycleCall(name) { 39 | this.props.shouldUpdate ? _updatingSetStateLifeCycleCalls.push(name) : _nonUpdatingSetStateLifeCycleCalls.push(name); 40 | this.recordStateChange(this.state.counter); 41 | } 42 | 43 | UNSAFE_componentWillReceiveProps(_) { 44 | this.recordLifecycleCall("componentWillReceiveProps"); 45 | } 46 | 47 | shouldComponentUpdate(_, __) { 48 | this.recordLifecycleCall("shouldComponentUpdate"); 49 | return this.props.shouldUpdate; 50 | } 51 | 52 | UNSAFE_componentWillUpdate(_, __) { 53 | this.recordLifecycleCall("componentWillUpdate"); 54 | } 55 | 56 | componentDidUpdate(_, __) { 57 | this.recordLifecycleCall("componentDidUpdate"); 58 | } 59 | 60 | outerSetStateCallback() { 61 | this.recordLifecycleCall('outerSetStateCallback'); 62 | } 63 | 64 | innerSetStateCallback() { 65 | this.recordLifecycleCall('innerSetStateCallback'); 66 | } 67 | 68 | outerTransactionalSetStateCallback(previousState, props) { 69 | this.recordLifecycleCall('outerTransactionalSetStateCallback'); 70 | return {counter: previousState.counter + 1}; 71 | } 72 | 73 | innerTransactionalSetStateCallback(previousState, props) { 74 | this.recordLifecycleCall('innerTransactionalSetStateCallback'); 75 | return {counter: previousState.counter + 1}; 76 | } 77 | 78 | handleOuterClick(_) { 79 | this.setState(this.outerTransactionalSetStateCallback.bind(this), this.outerSetStateCallback.bind(this)); 80 | } 81 | 82 | handleInnerClick(_) { 83 | this.setState(this.innerTransactionalSetStateCallback.bind(this), this.innerSetStateCallback.bind(this)); 84 | } 85 | 86 | render() { 87 | this.recordLifecycleCall('render'); 88 | return React.createElement("div", {onClick: this.handleOuterClick.bind(this)}, 89 | React.createElement("div", {onClick: this.handleInnerClick.bind(this)}, this.state.counter) 90 | ); 91 | } 92 | } 93 | 94 | ReactSetStateTestComponent.defaultProps = { shouldUpdate: true }; 95 | 96 | var updatingInstance = ReactDOM.render( 97 | React.createElement(ReactSetStateTestComponent), 98 | document.createElement("div") 99 | ); 100 | 101 | var nonUpdatingInstance = ReactDOM.render( 102 | React.createElement(ReactSetStateTestComponent, {shouldUpdate: false}), 103 | document.createElement("div") 104 | ); 105 | 106 | React.addons.TestUtils.Simulate.click(ReactDOM.findDOMNode(updatingInstance).children[0]); 107 | 108 | React.addons.TestUtils.Simulate.click(ReactDOM.findDOMNode(nonUpdatingInstance).children[0]); -------------------------------------------------------------------------------- /test/ReactSetStateTestComponent2.js: -------------------------------------------------------------------------------- 1 | function getComponent2UpdatingSetStateLifeCycleCalls() { 2 | return _component2UpdatingSetStateLifeCycleCalls; 3 | } 4 | 5 | var _component2UpdatingSetStateLifeCycleCalls = []; 6 | 7 | function getComponent2NonUpdatingSetStateLifeCycleCalls() { 8 | return _component2NonUpdatingSetStateLifeCycleCalls; 9 | } 10 | 11 | var _component2NonUpdatingSetStateLifeCycleCalls = []; 12 | 13 | function getComponent2LatestJSCounter() { 14 | return _component2Counter; 15 | } 16 | 17 | function getComponent2UpdatingRenderedCounter() { 18 | return ReactDOM.findDOMNode(updatingInstance).textContent; 19 | } 20 | 21 | function getComponent2NonUpdatingRenderedCounter() { 22 | return ReactDOM.findDOMNode(nonUpdatingInstance).textContent; 23 | } 24 | 25 | function getComponent2UpdatingErrorMessage() { 26 | return ReactDOM.findDOMNode(updatingInstance).textContent; 27 | } 28 | 29 | function getComponent2NonUpdatingErrorMessage() { 30 | return ReactDOM.findDOMNode(nonUpdatingInstance).textContent; 31 | } 32 | 33 | function staticLifecycleCallProxy(name, shouldUpdate){ 34 | shouldUpdate ? _component2UpdatingSetStateLifeCycleCalls.push(name) : _component2NonUpdatingSetStateLifeCycleCalls.push(name); 35 | } 36 | 37 | function getComponent2ErrorMessage(){ 38 | return _error.toString(); 39 | } 40 | 41 | function getComponent2ErrorInfo(){ 42 | return _info.componentStack; 43 | } 44 | 45 | function getComponent2ErrorFromDerivedState(){ 46 | return _errorFromGetDerivedState.toString(); 47 | } 48 | 49 | var _component2Counter; 50 | var _shouldThrow; 51 | var _shouldUpdate; 52 | var _error; 53 | var _info; 54 | var _errorFromGetDerivedState; 55 | 56 | class ReactSetStateTestComponent2 extends React.Component { 57 | constructor(props) { 58 | super(props); 59 | _component2Counter = 1; 60 | _shouldThrow = true; 61 | _shouldUpdate = props.shouldUpdate; 62 | this.state = {counter: _component2Counter, shouldThrow: _shouldThrow, error: '', info: '', errorFromGetDerivedState: ''}; 63 | } 64 | 65 | recordStateChange(newCount) { 66 | _component2Counter = newCount; 67 | } 68 | 69 | recordLifecycleCall(name) { 70 | this.props.shouldUpdate ? _component2UpdatingSetStateLifeCycleCalls.push(name) : _component2NonUpdatingSetStateLifeCycleCalls.push(name); 71 | this.recordStateChange(this.state.counter); 72 | } 73 | 74 | static getDerivedStateFromProps(nextProps, __) { 75 | _shouldUpdate = nextProps.shouldUpdate; 76 | _shouldUpdate 77 | ? _component2UpdatingSetStateLifeCycleCalls.push("getDerivedStateFromProps") 78 | : _component2NonUpdatingSetStateLifeCycleCalls.push("getDerivedStateFromProps"); 79 | return null; 80 | } 81 | 82 | shouldComponentUpdate(_, __) { 83 | this.recordLifecycleCall("shouldComponentUpdate"); 84 | _shouldUpdate = this.props.shouldUpdate; 85 | return this.props.shouldUpdate; 86 | } 87 | 88 | getSnapshotBeforeUpdate(_, __) { 89 | this.recordLifecycleCall("getSnapshotBeforeUpdate"); 90 | return null; 91 | } 92 | 93 | componentDidUpdate(_, __, ___) { 94 | this.recordLifecycleCall("componentDidUpdate"); 95 | } 96 | 97 | outerSetStateCallback() { 98 | this.recordLifecycleCall('outerSetStateCallback'); 99 | } 100 | 101 | innerSetStateCallback() { 102 | this.recordLifecycleCall('innerSetStateCallback'); 103 | } 104 | 105 | outerTransactionalSetStateCallback(previousState, props) { 106 | this.recordLifecycleCall('outerTransactionalSetStateCallback'); 107 | return {counter: previousState.counter + 1}; 108 | } 109 | 110 | innerTransactionalSetStateCallback(previousState, props) { 111 | this.recordLifecycleCall('innerTransactionalSetStateCallback'); 112 | return {counter: previousState.counter + 1}; 113 | } 114 | 115 | handleOuterClick(_) { 116 | this.setState(this.outerTransactionalSetStateCallback.bind(this), this.outerSetStateCallback.bind(this)); 117 | } 118 | 119 | handleInnerClick(_) { 120 | this.setState(this.innerTransactionalSetStateCallback.bind(this), this.innerSetStateCallback.bind(this)); 121 | } 122 | 123 | componentDidCatch(error, info) { 124 | this.recordLifecycleCall('componentDidCatch'); 125 | _error = error; 126 | _info = info; 127 | this.setState({error, info}); 128 | } 129 | 130 | static getDerivedStateFromError(error) { 131 | staticLifecycleCallProxy('getDerivedStateFromError', _shouldUpdate); 132 | _errorFromGetDerivedState = error; 133 | return {shouldThrow: false, errorFromGetDerivedState: error}; 134 | } 135 | 136 | render() { 137 | this.recordLifecycleCall('render'); 138 | if (!this.state.shouldThrow) { 139 | return React.createElement("div", {onClick: this.handleOuterClick.bind(this)}, 140 | [ 141 | React.createElement("div", {onClick: this.handleInnerClick.bind(this), key: 'c1'}, this.state.counter), 142 | React.createElement("div", {onClick: this.handleInnerClick.bind(this), key: 'c2'}, this.state.error.toString()), 143 | React.createElement("div", {onClick: this.handleInnerClick.bind(this), key: 'c3'}, this.state.info.toString()), 144 | React.createElement("div", {onClick: this.handleInnerClick.bind(this), key: 'c4'}, this.state.errorFromGetDerivedState.toString()), 145 | ] 146 | ); 147 | } else { 148 | return React.createElement("div", {onClick: this.handleOuterClick.bind(this)}, 149 | [ 150 | React.createElement("div", {onClick: this.handleInnerClick.bind(this), key: 'c1'}, this.state.counter), 151 | React.createElement(ErrorComponent, {key: 'c2'}, null) 152 | ] 153 | ); 154 | } 155 | } 156 | } 157 | 158 | 159 | class ErrorComponent extends React.Component { 160 | throwError() { 161 | throw "It crashed!"; 162 | } 163 | 164 | render() { 165 | this.throwError(); 166 | return React.createElement("div", {}, "Error"); 167 | } 168 | } 169 | 170 | 171 | ReactSetStateTestComponent2.defaultProps = { shouldUpdate: true }; 172 | 173 | var reactComponent2UpdatingInstance = ReactDOM.render( 174 | React.createElement(ReactSetStateTestComponent2), 175 | document.createElement("div") 176 | ); 177 | 178 | var reactComponent2NonUpdatingInstance = ReactDOM.render( 179 | React.createElement(ReactSetStateTestComponent2, {shouldUpdate: false}), 180 | document.createElement("div") 181 | ); 182 | 183 | React.addons.TestUtils.Simulate.click(ReactDOM.findDOMNode(reactComponent2UpdatingInstance).children[0]); 184 | 185 | React.addons.TestUtils.Simulate.click(ReactDOM.findDOMNode(reactComponent2NonUpdatingInstance).children[0]); 186 | -------------------------------------------------------------------------------- /test/factory/dart_factory_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.dart_factory_test; 3 | 4 | // ignore_for_file: deprecated_member_use_from_same_package 5 | // ignore_for_file: invalid_use_of_protected_member 6 | import 'package:react/react_client/react_interop.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | import 'package:react/react.dart' as react; 10 | import 'package:react/react_client.dart'; 11 | 12 | import 'common_factory_tests.dart'; 13 | 14 | main() { 15 | group('ReactDartComponentFactoryProxy', () { 16 | test('has a type corresponding to the backing JS class', () { 17 | expect(Foo.type, equals(Foo.reactClass)); 18 | }); 19 | 20 | group('- common factory behavior -', () { 21 | group('Component', () { 22 | group('- common factory behavior -', () { 23 | commonFactoryTests(Foo, dartComponentVersion: ReactDartComponentVersion.component); 24 | }); 25 | 26 | group('- refs -', () { 27 | refTests<_Foo>( 28 | Foo, 29 | verifyRefValue: (ref) { 30 | expect(ref, isA<_Foo>()); 31 | }, 32 | verifyJsRefValue: (ref) { 33 | expect(ref, isA().having((c) => c.dartComponent, 'dartComponent', isA<_Foo>())); 34 | }, 35 | ); 36 | }); 37 | }); 38 | 39 | group('Component2', () { 40 | group('- common factory behavior -', () { 41 | commonFactoryTests(Foo2, dartComponentVersion: ReactDartComponentVersion.component2); 42 | }); 43 | 44 | group('- refs -', () { 45 | refTests<_Foo2>( 46 | Foo2, 47 | verifyRefValue: (ref) { 48 | expect(ref, isA<_Foo2>()); 49 | }, 50 | verifyJsRefValue: (ref) { 51 | expect(ref, isA().having((c) => c.dartComponent, 'dartComponent', isA<_Foo2>())); 52 | }, 53 | ); 54 | }); 55 | }); 56 | }); 57 | }); 58 | } 59 | 60 | final Foo = react.registerComponent(() => _Foo()) as ReactDartComponentFactoryProxy; 61 | 62 | class _Foo extends react.Component { 63 | @override 64 | render() { 65 | props['onDartRender']?.call(props); 66 | return react.div({...props, 'ref': props['forwardedRef']}); 67 | } 68 | } 69 | 70 | final Foo2 = react.registerComponent2(() => _Foo2()); 71 | 72 | class _Foo2 extends react.Component2 { 73 | @override 74 | render() { 75 | props['onDartRender']?.call(props); 76 | return react.div({...props, 'ref': props['forwardedRef']}); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/factory/dart_function_factory_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | // ignore_for_file: invalid_use_of_protected_member 3 | @TestOn('browser') 4 | library react.dart_function_factory_test; 5 | 6 | import 'package:react/react.dart' as react; 7 | import 'package:react/react_client/react_interop.dart'; 8 | import 'package:react/src/js_interop_util.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'common_factory_tests.dart'; 12 | 13 | main() { 14 | group('ReactDartFunctionComponentFactoryProxy', () { 15 | test('has a type corresponding to the backing JS Function', () { 16 | expect(FunctionFoo.type, equals(FunctionFoo.reactFunction)); 17 | }); 18 | 19 | group('Function Component -', () { 20 | group('- common factory behavior -', () { 21 | commonFactoryTests( 22 | FunctionFoo, 23 | dartComponentVersion: ReactDartComponentVersion.component2, 24 | ); 25 | }); 26 | 27 | group('displayName', () { 28 | test('gets automatically populated when not provided', () { 29 | expect(FunctionFoo.displayName, '_FunctionFoo'); 30 | 31 | expect(getJsFunctionName(FunctionFoo.reactFunction), '_FunctionFoo'); 32 | 33 | expect(FunctionFoo.displayName, getJsFunctionName(FunctionFoo.reactFunction)); 34 | }); 35 | 36 | test('is populated by the provided argument', () { 37 | expect(NamedFunctionFoo.displayName, 'Bar'); 38 | 39 | expect(getJsFunctionName(NamedFunctionFoo.reactFunction), 'Bar'); 40 | 41 | expect(NamedFunctionFoo.displayName, getJsFunctionName(NamedFunctionFoo.reactFunction)); 42 | }); 43 | }); 44 | }); 45 | }); 46 | } 47 | 48 | final NamedFunctionFoo = react.registerFunctionComponent(_FunctionFoo, displayName: 'Bar'); 49 | 50 | final FunctionFoo = react.registerFunctionComponent(_FunctionFoo); 51 | 52 | _FunctionFoo(Map props) { 53 | props['onDartRender']?.call(props); 54 | return react.div({}); 55 | } 56 | -------------------------------------------------------------------------------- /test/factory/dom_factory_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.dom_factory_test; 3 | 4 | import 'dart:html'; 5 | 6 | import 'package:test/test.dart'; 7 | 8 | import 'package:react/react.dart' as react; 9 | 10 | import 'common_factory_tests.dart'; 11 | 12 | main() { 13 | group('ReactDomComponentFactoryProxy', () { 14 | group('- common factory behavior -', () { 15 | commonFactoryTests(react.div); 16 | }); 17 | 18 | group('- dom event handler wrapping -', () { 19 | domEventHandlerWrappingTests(react.div); 20 | }); 21 | 22 | group('- refs -', () { 23 | refTests(react.span, verifyRefValue: (ref) { 24 | expect(ref, TypeMatcher()); 25 | }); 26 | }); 27 | 28 | test('has a type corresponding to the DOM tagName', () { 29 | expect(react.div.type, 'div'); 30 | expect(react.span.type, 'span'); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/factory/js_factory_test.dart: -------------------------------------------------------------------------------- 1 | @JS() 2 | @TestOn('browser') 3 | library react.js_factory_test; 4 | 5 | import 'dart:html'; 6 | 7 | import 'package:js/js.dart'; 8 | import 'package:react/react.dart' as react; 9 | import 'package:react/react_client.dart'; 10 | import 'package:react/react_client/react_interop.dart'; 11 | import 'package:react/react_test_utils.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | import 'common_factory_tests.dart'; 15 | 16 | main() { 17 | group('ReactJsComponentFactoryProxy', () { 18 | group('(class component)', () { 19 | group('- common factory behavior -', () { 20 | commonFactoryTests(JsFoo); 21 | }); 22 | 23 | group('- dom event handler wrapping -', () { 24 | domEventHandlerWrappingTests(JsFoo); 25 | }); 26 | 27 | group('- refs -', () { 28 | refTests(JsFoo, verifyRefValue: (ref) { 29 | expect(isCompositeComponentWithTypeV2(ref, JsFoo), isTrue); 30 | }); 31 | }); 32 | 33 | test('has a type corresponding to the backing JS class', () { 34 | expect(JsFoo.type, equals(_JsFoo)); 35 | }); 36 | }); 37 | 38 | group('(function component)', () { 39 | group('- common factory behavior -', () { 40 | commonFactoryTests(JsFooFunction); 41 | }); 42 | 43 | group('- dom event handler wrapping -', () { 44 | domEventHandlerWrappingTests(JsFooFunction); 45 | }); 46 | 47 | group('- refs -', () { 48 | // This function component forwards the ref to DOM node 49 | refTests(JsFooFunction, verifyRefValue: (ref) { 50 | expect(ref, isA()); 51 | }); 52 | }); 53 | 54 | test('has a type corresponding to the backing JS class', () { 55 | expect(JsFooFunction.type, equals(_JsFooFunction)); 56 | }); 57 | }); 58 | 59 | test('with no children returns JS undefined', () { 60 | expect(hasUndefinedChildren(react.div({})), isTrue); 61 | expect(hasUndefinedChildren(react.div({}, jsNull)), isFalse, 62 | reason: 'Sanity check that JS `null` is not the same as JS `undefined`'); 63 | }); 64 | }); 65 | } 66 | 67 | @JS() 68 | external ReactClass get _JsFoo; 69 | final JsFoo = ReactJsComponentFactoryProxy(_JsFoo); 70 | 71 | @JS() 72 | external ReactClass get _JsFooFunction; 73 | final JsFooFunction = ReactJsComponentFactoryProxy(_JsFooFunction); 74 | 75 | @JS() 76 | external bool Function(ReactElement) get hasUndefinedChildren; 77 | -------------------------------------------------------------------------------- /test/forward_ref_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.forward_ref_test; 3 | 4 | // ignore_for_file: deprecated_member_use_from_same_package 5 | import 'dart:js_util'; 6 | 7 | import 'package:react/react.dart' as react; 8 | import 'package:react/react_client/react_interop.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'factory/common_factory_tests.dart'; 12 | 13 | main() { 14 | group('forwardRef2', () { 15 | group('- common factory behavior -', () { 16 | final ForwardRef2Test = react.forwardRef2((props, ref) { 17 | props['onDartRender']?.call(props); 18 | props['onDartRenderWithRef']?.call(props, ref); 19 | return react.div({...props, 'ref': ref}); 20 | }); 21 | 22 | commonFactoryTests( 23 | ForwardRef2Test, 24 | // ignore: invalid_use_of_protected_member 25 | dartComponentVersion: ReactDartComponentVersion.component2, 26 | ); 27 | }); 28 | 29 | // Ref behavior is tested functionally for all factory types in commonFactoryTests 30 | 31 | group('sets name on the rendered component as expected', () { 32 | test('unless the displayName argument is not passed to forwardRef2', () { 33 | final ForwardRefTestComponent = forwardRef2((props, ref) {}); 34 | expect(getProperty(getProperty(ForwardRefTestComponent.type as Object, 'render'), 'name'), anyOf('', isNull)); 35 | }); 36 | 37 | test('when displayName argument is passed to forwardRef2', () { 38 | const name = 'ForwardRefTestComponent'; 39 | final ForwardRefTestComponent = forwardRef2((props, ref) { 40 | return null; 41 | }, displayName: name); 42 | expect(getProperty(getProperty(ForwardRefTestComponent.type as Object, 'render'), 'name'), name); 43 | }); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/js_builds/react17/README: -------------------------------------------------------------------------------- 1 | # js_builds tests 2 | 3 | The same Dart tests in `shared_tests.dart` are run using the different HTML files in this directory. 4 | 5 | These HTML files import different combinations of the different consumable React JS files exposed by this package, 6 | testing that they all include the JS functions needed by Dart code, as well as testing build-specific JS functionality. 7 | -------------------------------------------------------------------------------- /test/js_builds/react17/console_spy_include_this_js_first.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This script must be included before react.js is so that it happens before 4 | // the wrapping of console.error that we'll be testing. 5 | 6 | (function() { 7 | window.consoleErrorCalls = []; 8 | 9 | const originalConsoleError = console.error; 10 | console.error = function spyingConsoleError() { 11 | originalConsoleError.apply(console, arguments); 12 | 13 | // Convert args to Array object so it interops nicely with Dart. 14 | const argsArray = [...arguments]; 15 | consoleErrorCalls.push(argsArray); 16 | }; 17 | })(); 18 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_dev_test.custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_dev_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | import 'package:react/react_client.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../shared_tests.dart'; 6 | 7 | main() { 8 | verifyJsFileLoaded('react.js'); 9 | verifyJsFileLoaded('react_dom.js'); 10 | 11 | group('React 17 JS files (dev build):', () { 12 | sharedConsoleFilteringTests(); 13 | 14 | sharedJsFunctionTests(); 15 | 16 | sharedErrorBoundaryComponentNameTests(); 17 | 18 | test('inReactDevMode', () { 19 | expect(inReactDevMode, isTrue); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_dev_with_addons_test.custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_dev_with_addons_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | import 'package:react/react_client.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../shared_tests.dart'; 6 | 7 | main() { 8 | verifyJsFileLoaded('react_with_addons.js'); 9 | verifyJsFileLoaded('react_dom.js'); 10 | 11 | group('React JS 17 files (dev w/ addons build):', () { 12 | sharedConsoleFilteringTests(); 13 | 14 | sharedJsFunctionTests(); 15 | 16 | sharedErrorBoundaryComponentNameTests(); 17 | 18 | test('inReactDevMode', () { 19 | expect(inReactDevMode, isTrue); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_prod_combined_test.custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_prod_combined_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | import 'package:react/react_client.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../shared_tests.dart'; 6 | 7 | main() { 8 | verifyJsFileLoaded('react_with_react_dom_prod.js'); 9 | 10 | group('React 17 JS files (prod combined build):', () { 11 | sharedConsoleFilteringTests(); 12 | 13 | sharedJsFunctionTests(); 14 | 15 | sharedErrorBoundaryComponentNameTests(); 16 | 17 | test('inReactDevMode', () { 18 | expect(inReactDevMode, isFalse); 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_prod_test.custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/js_builds/react17/js_prod_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | import 'package:react/react_client.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../shared_tests.dart'; 6 | 7 | main() { 8 | verifyJsFileLoaded('react_prod.js'); 9 | verifyJsFileLoaded('react_dom_prod.js'); 10 | 11 | group('React 17 JS files (prod build):', () { 12 | sharedConsoleFilteringTests(); 13 | 14 | sharedJsFunctionTests(); 15 | 16 | sharedErrorBoundaryComponentNameTests(); 17 | 18 | test('inReactDevMode', () { 19 | expect(inReactDevMode, isFalse); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/js_builds/react18/README: -------------------------------------------------------------------------------- 1 | # js_builds tests 2 | 3 | The same Dart tests in `shared_tests.dart` are run using the different HTML files in this directory. 4 | 5 | These HTML files import different combinations of the different consumable React JS files exposed by this package, 6 | testing that they all include the JS functions needed by Dart code, as well as testing build-specific JS functionality. 7 | -------------------------------------------------------------------------------- /test/js_builds/react18/console_spy_include_this_js_first.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This script must be included before react.js is so that it happens before 4 | // the wrapping of console.error that we'll be testing. 5 | 6 | (function() { 7 | window.consoleErrorCalls = []; 8 | 9 | const originalConsoleError = console.error; 10 | console.error = function spyingConsoleError() { 11 | originalConsoleError.apply(console, arguments); 12 | 13 | // Convert args to Array object so it interops nicely with Dart. 14 | const argsArray = [...arguments]; 15 | consoleErrorCalls.push(argsArray); 16 | }; 17 | })(); 18 | -------------------------------------------------------------------------------- /test/js_builds/react18/js_dev_test.custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ` 17 | -------------------------------------------------------------------------------- /test/js_builds/react18/js_dev_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | import 'package:react/react_client.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../shared_tests.dart'; 6 | 7 | main() { 8 | verifyJsFileLoaded('react.dev.js'); 9 | 10 | group('React 18 JS files (dev build):', () { 11 | sharedConsoleFilteringTests(); 12 | 13 | sharedJsFunctionTests(); 14 | 15 | sharedErrorBoundaryComponentNameTests(); 16 | 17 | test('inReactDevMode', () { 18 | expect(inReactDevMode, isTrue); 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/js_builds/react18/js_prod_test.custom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/js_builds/react18/js_prod_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | import 'package:react/react_client.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../shared_tests.dart'; 6 | 7 | main() { 8 | verifyJsFileLoaded('react.min.js'); 9 | 10 | group('React 18 JS files (prod build):', () { 11 | sharedConsoleFilteringTests(); 12 | 13 | sharedJsFunctionTests(); 14 | 15 | sharedErrorBoundaryComponentNameTests(); 16 | 17 | test('inReactDevMode', () { 18 | expect(inReactDevMode, isFalse); 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/lifecycle_test/util.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'package:matcher/matcher.dart'; 3 | import 'package:react/react.dart'; 4 | 5 | const Map defaultProps = {'defaultProp': 'default'}; 6 | const Map emptyChildrenProps = {'children': []}; 7 | 8 | Map matchCall(String memberName, {args = anything, props = anything, state = anything, context = anything}) { 9 | return { 10 | 'memberName': memberName, 11 | 'arguments': args, 12 | 'props': props, 13 | 'state': state, 14 | 'context': context, 15 | }; 16 | } 17 | 18 | /// A test helper to record lifecycle calls 19 | mixin LifecycleTestHelper on Component { 20 | static List staticLifecycleCalls = []; 21 | 22 | /// We needed to add [staticLifecycleCalls] to be able to test static 23 | /// lifecycle methods like `getDerivedStateFromProps` and 24 | /// `getDerivedStateFromError`, which don't get called on the same instance 25 | /// as other lifecycle methods. 26 | /// 27 | /// This allows static and instance lifecycle methods to add calls to the same list 28 | List get lifecycleCalls => staticLifecycleCalls; 29 | 30 | List get lifecycleCallMemberNames => staticLifecycleCalls.map((call) { 31 | return call['memberName']; 32 | }).toList(); 33 | 34 | T lifecycleCall(String memberName, 35 | {List arguments = const [], T Function()? defaultReturnValue, Map? staticProps}) { 36 | // Don't try to access late variables props/state/jsThis before they're initialized. 37 | const staticLifecycleMethods = { 38 | 'defaultProps', 39 | 'getDefaultProps', 40 | 'getDerivedStateFromProps', 41 | 'getDerivedStateFromError', 42 | }; 43 | final hasPropsInitialized = !staticLifecycleMethods.contains(memberName); 44 | final hasStateInitialized = !{ 45 | ...staticLifecycleMethods, 46 | 'initialState', 47 | 'getInitialState', 48 | }.contains(memberName); 49 | 50 | lifecycleCalls.add({ 51 | 'memberName': memberName, 52 | 'arguments': arguments, 53 | 'props': hasPropsInitialized ? Map.from(props) : null, 54 | 'state': hasStateInitialized ? Map.from(state) : null, 55 | 'context': context, 56 | }); 57 | 58 | final lifecycleCallback = (hasPropsInitialized ? props : (staticProps ?? {}))[memberName]; 59 | if (lifecycleCallback != null) { 60 | return Function.apply(lifecycleCallback as Function, [this, ...arguments]) as T; 61 | } 62 | 63 | if (defaultReturnValue != null) { 64 | return defaultReturnValue(); 65 | } 66 | 67 | return null as T; 68 | } 69 | 70 | void callSetStateWithNullValue() { 71 | setState(null); 72 | } 73 | } 74 | 75 | abstract class DefaultPropsCachingTestHelper implements Component { 76 | int get staticGetDefaultPropsCallCount; 77 | set staticGetDefaultPropsCallCount(int value); 78 | } 79 | -------------------------------------------------------------------------------- /test/nullable_callback_detection/non_null_safe_refs.dart: -------------------------------------------------------------------------------- 1 | //@dart=2.11 2 | 3 | import 'dart:html'; 4 | 5 | void elementRef(Element _) {} 6 | 7 | void dynamicRef(dynamic _) {} 8 | -------------------------------------------------------------------------------- /test/nullable_callback_detection/null_safe_refs.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | // Declare these separately so we can reference them from non-null-safe test files. 4 | 5 | void nonNullableElementRef(Element _) {} 6 | 7 | void nullableElementRef(Element? _) {} 8 | 9 | void dynamicRef(dynamic _) {} 10 | -------------------------------------------------------------------------------- /test/nullable_callback_detection/nullable_callback_detection_sound_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.test.unsound_null_safety_test; 3 | 4 | import 'package:react/src/react_client/factory_util.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'null_safe_refs.dart' as null_safe_refs; 8 | import 'sound_null_safety_detection.dart'; 9 | 10 | main() { 11 | // This test is the counterpart to nullable_callback_detection_unsound_test.dart, 12 | // to verify expected behavior under sound null safety. 13 | // See that file for more info. 14 | group('nullable callback detection, when compiled with sound null safety:', () { 15 | setUpAll(() { 16 | expect(hasUnsoundNullSafety, isFalse, reason: 'these tests should be running under sound null safety'); 17 | }); 18 | 19 | group('isRefArgumentDefinitelyNonNullable', () { 20 | test('returns true for non-nullable refs', () { 21 | expect(isRefArgumentDefinitelyNonNullable(null_safe_refs.nonNullableElementRef), isTrue); 22 | }); 23 | 24 | test('returns false for nullable refs', () { 25 | expect(isRefArgumentDefinitelyNonNullable(null_safe_refs.nullableElementRef), isFalse); 26 | expect(isRefArgumentDefinitelyNonNullable(null_safe_refs.dynamicRef), isFalse); 27 | }); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/nullable_callback_detection/nullable_callback_detection_unsound_test.dart: -------------------------------------------------------------------------------- 1 | // @dart=2.9 2 | @TestOn('browser') 3 | library react.test.unsound_null_safety_test; 4 | 5 | import 'package:react/src/react_client/factory_util.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'non_null_safe_refs.dart' as non_null_safe_refs; 9 | import 'null_safe_refs.dart' as null_safe_refs; 10 | import 'sound_null_safety_detection.dart'; 11 | 12 | main() { 13 | // Since our callback ref non-nullable argument detection relies on runtime type-checking behavior 14 | // that's different in null safety (`is Function(Null)`, we want to ensure we know how it behaves 15 | // both in sound and unsound null safety runtime environments. 16 | // 17 | // So, this test file simulates that setup. 18 | group('nullable callback detection, when compiled with unsound null safety:', () { 19 | setUpAll(() { 20 | expect(hasUnsoundNullSafety, isTrue, reason: 'these tests should be running under unsound null safety'); 21 | }); 22 | 23 | group('isRefArgumentDefinitelyNonNullable:', () { 24 | group('when refs are declared in null-safe code,', () { 25 | test('returns false, even for non-nullable refs', () { 26 | expect(isRefArgumentDefinitelyNonNullable(null_safe_refs.nonNullableElementRef), isFalse); 27 | }); 28 | 29 | test('returns false for nullable refs', () { 30 | expect(isRefArgumentDefinitelyNonNullable(null_safe_refs.nullableElementRef), isFalse); 31 | expect(isRefArgumentDefinitelyNonNullable(null_safe_refs.dynamicRef), isFalse); 32 | }); 33 | }); 34 | 35 | group('when refs are declared in non-null-safe code,', () { 36 | test('returns false', () { 37 | expect(isRefArgumentDefinitelyNonNullable(non_null_safe_refs.elementRef), isFalse); 38 | expect(isRefArgumentDefinitelyNonNullable(non_null_safe_refs.dynamicRef), isFalse); 39 | }); 40 | }); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/nullable_callback_detection/sound_null_safety_detection.dart: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/dart-lang/sdk/blob/252bfcb27aa14042ee07e081f25abf60cf6222b3/pkg/front_end/testcases/patterns/caching_constants.dart.textual_outline_modelled.expect#L3 2 | // Copyright 2012, the Dart project authors. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following 11 | // disclaimer in the documentation and/or other materials provided 12 | // with the distribution. 13 | // * Neither the name of Google LLC nor the names of its 14 | // contributors may be used to endorse or promote products derived 15 | // from this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | bool get hasUnsoundNullSafety => const [] is List; 29 | -------------------------------------------------------------------------------- /test/react_client/bridge_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.bridge_test; 3 | 4 | import 'package:react/react.dart' as react; 5 | import 'package:react/react_client/bridge.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import '../util.dart'; 9 | 10 | main() { 11 | // Test behavior that isn't functionally tested via component lifecycle tests. 12 | group('Component2Bridge', () { 13 | group('.forComponent returns the bridge used by that mounted component, which', () { 14 | test('by default is a const instance of Component2BridgeImpl', () { 15 | final instance = getDartComponent<_Foo>(render(Foo({}))); 16 | expect(Component2Bridge.forComponent(instance), same(const Component2BridgeImpl())); 17 | }); 18 | 19 | test('can be a custom bridge specified via registerComponent2', () { 20 | customBridgeCalls = []; 21 | addTearDown(() => customBridgeCalls = null); 22 | 23 | final instance = getDartComponent<_BridgeTest>(render(BridgeTest({}))); 24 | final bridge = Component2Bridge.forComponent(instance); 25 | expect(bridge, isNotNull); 26 | expect( 27 | customBridgeCalls, 28 | // Use `contains` since the static component instance and its bridge also get instantiated, 29 | // adding entries to customBridgeCalls. 30 | // `equals` is needed otherwise deep matching with matchers won't work 31 | contains(equals({ 32 | 'component': same(instance), 33 | 'returnedBridge': same(bridge), 34 | })), 35 | ); 36 | }); 37 | }); 38 | 39 | group('- Component2BridgeImpl', () { 40 | test('.bridgeFactory returns a const instance of Component2BridgeImpl', () { 41 | expect(Component2BridgeImpl.bridgeFactory(_Foo()), same(const Component2BridgeImpl())); 42 | }); 43 | }); 44 | }); 45 | } 46 | 47 | final Foo = react.registerComponent2(() => _Foo()); 48 | 49 | class _Foo extends react.Component2 { 50 | @override 51 | render() => react.div({}); 52 | } 53 | 54 | List? customBridgeCalls; 55 | 56 | final BridgeTest = react.registerComponent2(() => _BridgeTest(), bridgeFactory: (component) { 57 | final returnedBridge = CustomComponent2Bridge(); 58 | customBridgeCalls?.add({ 59 | 'component': component, 60 | 'returnedBridge': returnedBridge, 61 | }); 62 | return returnedBridge; 63 | }); 64 | 65 | class _BridgeTest extends react.Component2 { 66 | @override 67 | render() => react.div({}); 68 | } 69 | 70 | class CustomComponent2Bridge extends Component2BridgeImpl {} 71 | -------------------------------------------------------------------------------- /test/react_client/chain_refs_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react.chain_refs_test; 4 | 5 | import 'package:js/js.dart'; 6 | import 'package:react/react_client.dart'; 7 | import 'package:react/src/react_client/chain_refs.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | import '../util.dart'; 11 | 12 | main() { 13 | group('Ref chaining utils:', () { 14 | testRef(_) {} 15 | 16 | group('chainRefs', () { 17 | // Chaining and arg validation is tested via chainRefList. 18 | 19 | group('skips chaining and passes through arguments when', () { 20 | test('both arguments are null', () { 21 | expect(chainRefs(null, null), isNull); 22 | }); 23 | 24 | test('the first argument is null', () { 25 | expect(chainRefs(null, testRef), same(testRef)); 26 | }); 27 | 28 | test('the second argument is null', () { 29 | expect(chainRefs(testRef, null), same(testRef)); 30 | }); 31 | }); 32 | }); 33 | 34 | group('chainRefList', () { 35 | // Chaining is tested functionally in refTests with each component type and each ref type. 36 | 37 | group('skips chaining and passes through arguments when', () { 38 | test('the list is empty', () { 39 | expect(chainRefList([]), isNull); 40 | }); 41 | 42 | test('there is a single element in the list', () { 43 | expect(chainRefList([testRef]), same(testRef)); 44 | }); 45 | 46 | test('the list has only null values', () { 47 | expect(chainRefList([null, null]), isNull); 48 | }); 49 | 50 | test('there is a single non-null element in the list', () { 51 | expect(chainRefList([null, testRef]), same(testRef)); 52 | }); 53 | }); 54 | 55 | group('raises an assertion when inputs are invalid:', () { 56 | test('strings', () { 57 | expect(() => chainRefList([testRef, 'bad ref']), 58 | throwsA(isA().havingMessage('String refs cannot be chained'))); 59 | }); 60 | 61 | test('unsupported function types', () { 62 | expect(() => chainRefList([testRef, () {}]), 63 | throwsA(isA().havingMessage('callback refs must take a single argument'))); 64 | }); 65 | 66 | test('other objects', () { 67 | expect(() => chainRefList([testRef, Object()]), 68 | throwsA(isA().havingMessage(contains('Invalid ref type')))); 69 | }); 70 | 71 | // test JS interop objects since type-checking anonymous interop objects 72 | test('non-createRef anonymous JS interop objects', () { 73 | expect(() => chainRefList([testRef, JsTypeAnonymous()]), 74 | throwsA(isA().havingMessage(contains('Invalid ref type')))); 75 | }); 76 | 77 | // test JS interop objects since type-checking anonymous interop objects 78 | test('non-createRef JS interop objects', () { 79 | expect(() => chainRefList([testRef, JsType()]), 80 | throwsA(isA().havingMessage(contains('Invalid ref type')))); 81 | }); 82 | 83 | test('callback refs with non-nullable arguments', () { 84 | expect(() => chainRefList([testRef, nonNullableCallbackRef]), throwsNonNullableCallbackRefAssertionError); 85 | }); 86 | }, tags: 'no-dart2js'); 87 | 88 | test('does not raise an assertion for valid input refs', () { 89 | final testCases = RefTestCaseCollection().createAllCases(); 90 | expect(() => chainRefList(testCases.map((r) => r.ref).toList()), returnsNormally); 91 | }); 92 | }); 93 | }); 94 | } 95 | 96 | @JS() 97 | @anonymous 98 | class JsTypeAnonymous { 99 | external factory JsTypeAnonymous(); 100 | } 101 | 102 | @JS() 103 | class JsType { 104 | external JsType(); 105 | } 106 | 107 | extension on TypeMatcher { 108 | TypeMatcher havingMessage(dynamic matcher) => having((e) => e.message, 'message', matcher); 109 | } 110 | -------------------------------------------------------------------------------- /test/react_client/react_interop_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.react_interop_test; 3 | 4 | import 'dart:html'; 5 | 6 | import 'package:react/react_client/react_interop.dart'; 7 | import 'package:react/react_dom.dart' as react_dom; 8 | import 'package:test/test.dart'; 9 | 10 | main() { 11 | group('ReactDom.createPortal', () { 12 | group('creates a portal ReactNode that renders correctly', () { 13 | late Element mountNode; 14 | 15 | setUp(() { 16 | mountNode = DivElement(); 17 | }); 18 | 19 | tearDown(() { 20 | react_dom.unmountComponentAtNode(mountNode); 21 | }); 22 | 23 | test('with simple children', () { 24 | final portalTargetNode = Element.div(); 25 | final portal = ReactDom.createPortal('foo', portalTargetNode); 26 | react_dom.render(portal, mountNode); 27 | 28 | expect(portalTargetNode.text, contains('foo')); 29 | }); 30 | 31 | test('with nested list children', () { 32 | final portalTargetNode = Element.div(); 33 | final portal = ReactDom.createPortal([ 34 | ['foo'], 35 | [ 36 | 'bar', 37 | ['baz'] 38 | ] 39 | ], portalTargetNode); 40 | react_dom.render(portal, mountNode); 41 | 42 | expect(portalTargetNode.text, contains('foobarbaz')); 43 | }); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/react_component_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | @TestOn('browser') 3 | 4 | import 'package:react/react.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | main() { 8 | group('Component2', () { 9 | late Component2 component; 10 | 11 | setUpAll(() { 12 | component = MyComponent(); 13 | }); 14 | 15 | group('throws when unsupported lifecycle methods are called (e.g., via super-calls)', () { 16 | test('getInitialState', () { 17 | expect(() => component.getInitialState(), throwsUnsupportedError); 18 | }); 19 | test('getDefaultProps', () { 20 | expect(() => component.getDefaultProps(), throwsUnsupportedError); 21 | }); 22 | test('componentWillMount', () { 23 | expect(() => component.componentWillMount(), throwsUnsupportedError); 24 | }); 25 | test('componentWillReceiveProps', () { 26 | expect(() => component.componentWillReceiveProps({}), throwsUnsupportedError); 27 | }); 28 | test('componentWillUpdate', () { 29 | expect(() => component.componentWillUpdate({}, {}), throwsUnsupportedError); 30 | }); 31 | test('getChildContext', () { 32 | expect(() => component.getChildContext(), throwsUnsupportedError); 33 | }); 34 | test('shouldComponentUpdateWithContext', () { 35 | expect(() => component.shouldComponentUpdateWithContext({}, {}, null), throwsUnsupportedError); 36 | }); 37 | test('componentWillUpdateWithContext', () { 38 | expect(() => component.componentWillUpdateWithContext({}, {}, null), throwsUnsupportedError); 39 | }); 40 | test('componentWillReceivePropsWithContext', () { 41 | expect(() => component.componentWillReceivePropsWithContext({}, null), throwsUnsupportedError); 42 | }); 43 | }); 44 | 45 | group('throws when unsupported members inherited from Component are used', () { 46 | test('replaceState', () { 47 | expect(() => component.replaceState(null), throwsUnsupportedError); 48 | }); 49 | test('childContextKeys getter', () { 50 | expect(() => component.childContextKeys, throwsUnsupportedError); 51 | }); 52 | test('contextKeys getter', () { 53 | expect(() => component.contextKeys, throwsUnsupportedError); 54 | }); 55 | test('initComponentInternal', () { 56 | expect(() => component.initComponentInternal({}, () {}), throwsUnsupportedError); 57 | }); 58 | test('initStateInternal', () { 59 | expect(() => component.initStateInternal(), throwsUnsupportedError); 60 | }); 61 | test('nextContext getter', () { 62 | expect(() => component.nextContext, throwsUnsupportedError); 63 | }); 64 | test('nextContext setter', () { 65 | expect(() => component.nextContext = null, throwsUnsupportedError); 66 | }); 67 | test('prevContext getter', () { 68 | expect(() => component.prevContext, throwsUnsupportedError); 69 | }); 70 | test('prevContext setter', () { 71 | expect(() => component.prevContext = null, throwsUnsupportedError); 72 | }); 73 | test('prevState getter', () { 74 | expect(() => component.prevState, throwsUnsupportedError); 75 | }); 76 | test('nextState getter', () { 77 | expect(() => component.nextState, throwsUnsupportedError); 78 | }); 79 | test('nextProps getter', () { 80 | expect(() => component.nextProps, throwsUnsupportedError); 81 | }); 82 | test('transferComponentState', () { 83 | expect(() => component.transferComponentState(), throwsUnsupportedError); 84 | }); 85 | test('ref getter', () { 86 | expect(() => component.ref, throwsUnsupportedError); 87 | }); 88 | test('setStateCallbacks getter', () { 89 | expect(() => component.setStateCallbacks, throwsUnsupportedError); 90 | }); 91 | test('transactionalSetStateCallbacks getter', () { 92 | expect(() => component.transactionalSetStateCallbacks, throwsUnsupportedError); 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | class MyComponent extends Component2 { 99 | @override 100 | render() => null; 101 | } 102 | -------------------------------------------------------------------------------- /test/react_context_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react_test_utils_test; 4 | 5 | import 'package:js/js.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'package:react/react.dart' as react; 9 | 10 | import 'shared_type_tester.dart'; 11 | import 'util.dart'; 12 | 13 | main() { 14 | void testTypeValue(dynamic typeToTest) { 15 | var contextTypeRef; 16 | var consumerRef; 17 | render(ContextProviderWrapper({ 18 | 'contextToUse': TestContext, 19 | 'value': typeToTest, 20 | }, [ 21 | ContextTypeComponent({ 22 | 'ref': (ref) { 23 | contextTypeRef = ref; 24 | } 25 | }), 26 | ContextConsumerWrapper({ 27 | 'contextToUse': TestContext, 28 | 'ref': (ref) { 29 | consumerRef = ref; 30 | } 31 | }) 32 | ])); 33 | expect(contextTypeRef.context, same(typeToTest)); 34 | expect(consumerRef.latestValue, same(typeToTest)); 35 | } 36 | 37 | group('New Context API (Component2 only)', () { 38 | group('sets and retrieves values correctly:', () { 39 | sharedTypeTests(testTypeValue); 40 | }); 41 | 42 | group('calculateChangeBits argument does not throw when used (has no effect in React 18)', () { 43 | _ContextProviderWrapper? providerRef; 44 | _ContextConsumerWrapper? consumerEvenRef; 45 | _ContextConsumerWrapper? consumerOddRef; 46 | 47 | setUp(() { 48 | render(ContextProviderWrapper({ 49 | 'contextToUse': TestCalculateChangedBitsContext, 50 | 'mode': 'increment', 51 | 'ref': (ref) { 52 | providerRef = ref as _ContextProviderWrapper?; 53 | } 54 | }, [ 55 | ContextConsumerWrapper({ 56 | 'key': 'EvenContextConsumer', 57 | 'contextToUse': TestCalculateChangedBitsContext, 58 | 'unstable_observedBits': 1 << 2, 59 | 'ref': (ref) { 60 | consumerEvenRef = ref as _ContextConsumerWrapper?; 61 | } 62 | }), 63 | ContextConsumerWrapper({ 64 | 'key': 'OddContextConsumer', 65 | 'contextToUse': TestCalculateChangedBitsContext, 66 | 'unstable_observedBits': 1 << 3, 67 | 'ref': (ref) { 68 | consumerOddRef = ref as _ContextConsumerWrapper?; 69 | } 70 | }) 71 | ])); 72 | }); 73 | 74 | test('on first render', () { 75 | expect(consumerEvenRef!.latestValue, 1); 76 | expect(consumerOddRef!.latestValue, 1); 77 | }); 78 | 79 | test('on value updates', () { 80 | // Test common behavior between React 17 (calculateChangedBits working) 81 | // and React 18 (it having no effect). 82 | providerRef!.increment(); 83 | expect(consumerEvenRef!.latestValue, 2); 84 | providerRef!.increment(); 85 | expect(consumerOddRef!.latestValue, 3); 86 | providerRef!.increment(); 87 | expect(consumerEvenRef!.latestValue, 4); 88 | }); 89 | }); 90 | }); 91 | } 92 | 93 | int calculateChangedBits(currentValue, nextValue) { 94 | var result = 0; 95 | if (nextValue % 2 == 0) { 96 | // Bit for even values 97 | result |= 1 << 2; 98 | } 99 | if (nextValue % 3 == 0) { 100 | // Bit for odd values 101 | result |= 1 << 3; 102 | } 103 | return result; 104 | } 105 | 106 | // ignore: deprecated_member_use_from_same_package 107 | var TestCalculateChangedBitsContext = react.createContext(1, calculateChangedBits); 108 | 109 | var TestContext = react.createContext(); 110 | 111 | final ContextProviderWrapper = react.registerComponent2(() => _ContextProviderWrapper()); 112 | 113 | class _ContextProviderWrapper extends react.Component2 { 114 | @override 115 | get initialState { 116 | return {'counter': 1}; 117 | } 118 | 119 | increment() { 120 | setState({'counter': state['counter'] + 1}); 121 | } 122 | 123 | @override 124 | render() { 125 | return react.div({}, [ 126 | (props['contextToUse'] as react.Context) 127 | .Provider({'value': props['mode'] == 'increment' ? state['counter'] : props['value']}, props['children']) 128 | ]); 129 | } 130 | } 131 | 132 | final ContextConsumerWrapper = react.registerComponent2(() => _ContextConsumerWrapper()); 133 | 134 | class _ContextConsumerWrapper extends react.Component2 { 135 | dynamic latestValue; 136 | @override 137 | render() { 138 | return (props['contextToUse'] as react.Context).Consumer({'unstable_observedBits': props['unstable_observedBits']}, 139 | (value) { 140 | latestValue = value; 141 | return react.div({}, '$value'); 142 | }); 143 | } 144 | } 145 | 146 | final ContextTypeComponent = react.registerComponent2(() => _ContextTypeComponent()); 147 | 148 | class _ContextTypeComponent extends react.Component2 { 149 | @override 150 | var contextType = TestContext; 151 | 152 | @override 153 | render() { 154 | return react.div({}, '$context'); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/react_fragment_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react_test_utils_test; 4 | 5 | import 'dart:html'; 6 | 7 | import 'package:js/js.dart'; 8 | import 'package:react/react.dart' as react; 9 | import 'package:react/react_dom.dart' as react_dom; 10 | import 'package:test/test.dart'; 11 | 12 | main() { 13 | group('Fragment', () { 14 | test('renders nothing but its children', () { 15 | var wrappingDivRef; 16 | 17 | react_dom.render( 18 | react.div({ 19 | 'ref': (ref) { 20 | wrappingDivRef = ref; 21 | } 22 | }, [ 23 | react.Fragment({}, [ 24 | react.div({}), 25 | react.div({}), 26 | react.div({}), 27 | react.div({}), 28 | ]) 29 | ]), 30 | Element.div(), 31 | ); 32 | 33 | expect(wrappingDivRef.children, hasLength(4)); 34 | }); 35 | 36 | test('passes the key properly onto the fragment', () { 37 | var callCount = 0; 38 | 39 | final mountElement = Element.div(); 40 | 41 | react_dom.render( 42 | react.Fragment({ 43 | 'key': 1 44 | }, [ 45 | FragmentTestDummy({ 46 | 'onComponentDidMount': () { 47 | callCount++; 48 | } 49 | }) 50 | ]), 51 | mountElement); 52 | 53 | expect(callCount, 1); 54 | 55 | react_dom.render( 56 | react.Fragment({ 57 | 'key': 2 58 | }, [ 59 | FragmentTestDummy({ 60 | 'onComponentDidMount': () { 61 | callCount++; 62 | } 63 | }) 64 | ]), 65 | mountElement); 66 | 67 | expect(callCount, 2, reason: 'Dummy should have been remounted as a result of Fragment key changing'); 68 | }); 69 | }); 70 | } 71 | 72 | class _FragmentTestDummy extends react.Component2 { 73 | @override 74 | componentDidMount() { 75 | props['onComponentDidMount'](); 76 | } 77 | 78 | @override 79 | render() { 80 | return react.button(props, 'hi'); 81 | } 82 | } 83 | 84 | final FragmentTestDummy = react.registerComponent2(() => _FragmentTestDummy()); 85 | -------------------------------------------------------------------------------- /test/react_lazy_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react.react_lazy_test; 4 | 5 | import 'dart:async'; 6 | import 'dart:js_util'; 7 | 8 | import 'package:js/js.dart'; 9 | import 'package:react/hooks.dart'; 10 | import 'package:react/react.dart' as react; 11 | import 'package:react/react_test_utils.dart' as rtu; 12 | import 'package:react/react_client/component_factory.dart'; 13 | import 'package:react/react_client/react_interop.dart'; 14 | import 'package:test/test.dart'; 15 | 16 | import 'factory/common_factory_tests.dart'; 17 | 18 | main() { 19 | group('lazy', () { 20 | // Event more lazy behavior is tested in `react_suspense_test.dart` 21 | 22 | test('correctly throws errors from within load function to the closest error boundary', () async { 23 | const errorString = 'intentional future error'; 24 | final errors = []; 25 | final errorCompleter = Completer(); 26 | final ThrowingLazyTest = react.lazy(() async { 27 | throw Exception(errorString); 28 | }); 29 | onError(error, info) { 30 | errors.add([error, info]); 31 | errorCompleter.complete(); 32 | } 33 | 34 | expect( 35 | () => rtu.renderIntoDocument(_ErrorBoundary( 36 | {'onComponentDidCatch': onError}, react.Suspense({'fallback': 'Loading...'}, ThrowingLazyTest({})))), 37 | returnsNormally); 38 | await expectLater(errorCompleter.future, completes); 39 | expect(errors, hasLength(1)); 40 | expect(errors.first.first, isA().having((e) => e.toString(), 'message', contains(errorString))); 41 | expect(errors.first.last, isA()); 42 | }); 43 | 44 | group('Dart component', () { 45 | final LazyTest = react.lazy(() async => react.forwardRef2((props, ref) { 46 | useImperativeHandle(ref, () => TestImperativeHandle()); 47 | props['onDartRender']?.call(props); 48 | return react.div({...props}); 49 | })); 50 | 51 | group('- common factory behavior -', () { 52 | commonFactoryTests( 53 | LazyTest, 54 | // ignore: invalid_use_of_protected_member 55 | dartComponentVersion: ReactDartComponentVersion.component2, 56 | renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child), 57 | ); 58 | }); 59 | 60 | group('- dom event handler wrapping -', () { 61 | domEventHandlerWrappingTests(LazyTest); 62 | }); 63 | 64 | group('- refs -', () { 65 | refTests(LazyTest, verifyRefValue: (ref) { 66 | expect(ref, isA()); 67 | }); 68 | }); 69 | }); 70 | 71 | group('JS component', () { 72 | final LazyJsTest = react.lazy(() async => ReactJsComponentFactoryProxy(_JsFoo)); 73 | 74 | group('- common factory behavior -', () { 75 | commonFactoryTests( 76 | LazyJsTest, 77 | // ignore: invalid_use_of_protected_member 78 | dartComponentVersion: ReactDartComponentVersion.component2, 79 | // This isn't a Dart component, but it's detected as one by tests due to the factory's dartComponentVersion 80 | isNonDartComponentWithDartWrapper: true, 81 | renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child), 82 | ); 83 | }); 84 | 85 | group('- dom event handler wrapping -', () { 86 | domEventHandlerWrappingTests( 87 | LazyJsTest, 88 | // This isn't a Dart component, but it's detected as one by tests due to the factory's dartComponentVersion 89 | isNonDartComponentWithDartWrapper: true, 90 | ); 91 | }); 92 | 93 | group('- refs -', () { 94 | refTests(LazyJsTest, verifyRefValue: (ref) { 95 | expect(getProperty(ref as Object, 'constructor'), same(_JsFoo)); 96 | }); 97 | }); 98 | }); 99 | }); 100 | } 101 | 102 | class TestImperativeHandle {} 103 | 104 | @JS() 105 | external ReactClass get _JsFoo; 106 | 107 | final _ErrorBoundary = react.registerComponent2(() => _ErrorBoundaryComponent(), skipMethods: []); 108 | 109 | class _ErrorBoundaryComponent extends react.Component2 { 110 | @override 111 | get initialState => {'hasError': false}; 112 | 113 | @override 114 | getDerivedStateFromError(dynamic error) => {'hasError': true}; 115 | 116 | @override 117 | componentDidCatch(dynamic error, ReactErrorInfo info) { 118 | props['onComponentDidCatch'](error, info); 119 | } 120 | 121 | @override 122 | render() { 123 | return (state['hasError'] as bool) ? null : props['children']; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/react_memo_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.react_memo_test; 3 | 4 | import 'dart:html'; 5 | 6 | import 'package:react/react.dart' as react; 7 | import 'package:react/react_client.dart'; 8 | import 'package:react/react_client/react_interop.dart'; 9 | import 'package:react/react_test_utils.dart' as rtu; 10 | import 'package:test/test.dart'; 11 | 12 | import 'factory/common_factory_tests.dart'; 13 | 14 | main() { 15 | group('memo2', () { 16 | sharedMemoTests(react.memo2); 17 | 18 | test('can be passed a forwardRef component (regression test)', () { 19 | late ReactComponentFactoryProxy factory; 20 | expect(() => factory = react.memo2(react.forwardRef2((props, ref) => 'foo')), returnsNormally); 21 | expect(() => rtu.renderIntoDocument(factory({})), returnsNormally); 22 | }); 23 | 24 | group('- common factory behavior -', () { 25 | final Memo2Test = react.memo2(react.registerFunctionComponent((props) { 26 | props['onDartRender']?.call(props); 27 | return react.div({...props}); 28 | })); 29 | 30 | commonFactoryTests( 31 | Memo2Test, 32 | // ignore: invalid_use_of_protected_member 33 | dartComponentVersion: ReactDartComponentVersion.component2, 34 | ); 35 | }); 36 | }); 37 | } 38 | 39 | typedef MemoFunction = react.ReactComponentFactoryProxy Function(ReactDartFunctionComponentFactoryProxy, 40 | {bool Function(Map, Map)? areEqual}); 41 | 42 | void sharedMemoTests(MemoFunction memoFunction) { 43 | late Ref<_MemoTestWrapperComponent?> memoTestWrapperComponentRef; 44 | late Ref localCountDisplayRef; 45 | late Ref valueMemoShouldIgnoreViaAreEqualDisplayRef; 46 | late int childMemoRenderCount; 47 | 48 | void renderMemoTest({ 49 | bool testAreEqual = false, 50 | }) { 51 | final customAreEqualFn = !testAreEqual 52 | ? null 53 | : (prevProps, nextProps) { 54 | return prevProps['localCount'] == nextProps['localCount']; 55 | }; 56 | 57 | final MemoTest = memoFunction(react.registerFunctionComponent((props) { 58 | childMemoRenderCount++; 59 | return react.div( 60 | {}, 61 | react.p( 62 | {'ref': localCountDisplayRef}, 63 | props['localCount'], 64 | ), 65 | react.p( 66 | {'ref': valueMemoShouldIgnoreViaAreEqualDisplayRef}, 67 | props['valueMemoShouldIgnoreViaAreEqual'], 68 | ), 69 | ); 70 | }), areEqual: customAreEqualFn); 71 | 72 | rtu.renderIntoDocument(MemoTestWrapper({ 73 | 'ref': memoTestWrapperComponentRef, 74 | 'memoComponentFactory': MemoTest, 75 | })); 76 | 77 | expect(localCountDisplayRef.current, isNotNull, reason: 'test setup sanity check'); 78 | expect(valueMemoShouldIgnoreViaAreEqualDisplayRef.current, isNotNull, reason: 'test setup sanity check'); 79 | expect(memoTestWrapperComponentRef.current!.redrawCount, 0, reason: 'test setup sanity check'); 80 | expect(childMemoRenderCount, 1, reason: 'test setup sanity check'); 81 | expect(memoTestWrapperComponentRef.current!.state['localCount'], 0, reason: 'test setup sanity check'); 82 | expect(memoTestWrapperComponentRef.current!.state['valueMemoShouldIgnoreViaAreEqual'], 0, 83 | reason: 'test setup sanity check'); 84 | expect(memoTestWrapperComponentRef.current!.state['valueMemoShouldNotKnowAbout'], 0, 85 | reason: 'test setup sanity check'); 86 | } 87 | 88 | setUp(() { 89 | memoTestWrapperComponentRef = react.createRef<_MemoTestWrapperComponent>(); 90 | localCountDisplayRef = react.createRef(); 91 | valueMemoShouldIgnoreViaAreEqualDisplayRef = react.createRef(); 92 | childMemoRenderCount = 0; 93 | }); 94 | 95 | group('renders its child component when props change', () { 96 | test('', () { 97 | renderMemoTest(); 98 | 99 | memoTestWrapperComponentRef.current!.increaseLocalCount(); 100 | expect(memoTestWrapperComponentRef.current!.state['localCount'], 1, reason: 'test setup sanity check'); 101 | expect(memoTestWrapperComponentRef.current!.redrawCount, 1, reason: 'test setup sanity check'); 102 | 103 | expect(childMemoRenderCount, 2); 104 | expect(localCountDisplayRef.current!.text, '1'); 105 | 106 | memoTestWrapperComponentRef.current!.increaseValueMemoShouldIgnoreViaAreEqual(); 107 | expect(memoTestWrapperComponentRef.current!.state['valueMemoShouldIgnoreViaAreEqual'], 1, 108 | reason: 'test setup sanity check'); 109 | expect(memoTestWrapperComponentRef.current!.redrawCount, 2, reason: 'test setup sanity check'); 110 | 111 | expect(childMemoRenderCount, 3); 112 | expect(valueMemoShouldIgnoreViaAreEqualDisplayRef.current!.text, '1'); 113 | }); 114 | 115 | test('unless the areEqual argument is set to a function that customizes when re-renders occur', () { 116 | renderMemoTest(testAreEqual: true); 117 | 118 | memoTestWrapperComponentRef.current!.increaseValueMemoShouldIgnoreViaAreEqual(); 119 | expect(memoTestWrapperComponentRef.current!.state['valueMemoShouldIgnoreViaAreEqual'], 1, 120 | reason: 'test setup sanity check'); 121 | expect(memoTestWrapperComponentRef.current!.redrawCount, 1, reason: 'test setup sanity check'); 122 | 123 | expect(childMemoRenderCount, 1); 124 | expect(valueMemoShouldIgnoreViaAreEqualDisplayRef.current!.text, '0'); 125 | }); 126 | }); 127 | 128 | test('does not re-render its child component when parent updates and props remain the same', () { 129 | renderMemoTest(); 130 | 131 | memoTestWrapperComponentRef.current!.increaseValueMemoShouldNotKnowAbout(); 132 | expect(memoTestWrapperComponentRef.current!.state['valueMemoShouldNotKnowAbout'], 1, 133 | reason: 'test setup sanity check'); 134 | expect(memoTestWrapperComponentRef.current!.redrawCount, 1, reason: 'test setup sanity check'); 135 | 136 | expect(childMemoRenderCount, 1); 137 | }); 138 | } 139 | 140 | final MemoTestWrapper = react.registerComponent2(() => _MemoTestWrapperComponent()); 141 | 142 | class _MemoTestWrapperComponent extends react.Component2 { 143 | int redrawCount = 0; 144 | 145 | @override 146 | get initialState => { 147 | 'localCount': 0, 148 | 'valueMemoShouldIgnoreViaAreEqual': 0, 149 | 'valueMemoShouldNotKnowAbout': 0, 150 | }; 151 | 152 | @override 153 | componentDidUpdate(Map prevProps, Map prevState, [dynamic snapshot]) { 154 | redrawCount++; 155 | } 156 | 157 | void increaseLocalCount() { 158 | setState({'localCount': state['localCount'] + 1}); 159 | } 160 | 161 | void increaseValueMemoShouldIgnoreViaAreEqual() { 162 | setState({'valueMemoShouldIgnoreViaAreEqual': state['valueMemoShouldIgnoreViaAreEqual'] + 1}); 163 | } 164 | 165 | void increaseValueMemoShouldNotKnowAbout() { 166 | setState({'valueMemoShouldNotKnowAbout': state['valueMemoShouldNotKnowAbout'] + 1}); 167 | } 168 | 169 | @override 170 | render() { 171 | return react.div( 172 | {}, 173 | props['memoComponentFactory']({ 174 | 'localCount': state['localCount'], 175 | 'valueMemoShouldIgnoreViaAreEqual': state['valueMemoShouldIgnoreViaAreEqual'], 176 | }), 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/react_strictmode_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react_test_utils_test; 4 | 5 | import 'dart:html'; 6 | 7 | import 'package:js/js.dart'; 8 | import 'package:react/react.dart' as react; 9 | import 'package:react/react_dom.dart' as react_dom; 10 | import 'package:test/test.dart'; 11 | 12 | main() { 13 | group('StrictMode', () { 14 | test('renders nothing but its children', () { 15 | var wrappingDivRef; 16 | 17 | react_dom.render( 18 | react.div({ 19 | 'ref': (ref) { 20 | wrappingDivRef = ref; 21 | } 22 | }, [ 23 | react.StrictMode({}, [ 24 | react.div({}), 25 | react.div({}), 26 | react.div({}), 27 | react.div({}), 28 | ]) 29 | ]), 30 | Element.div(), 31 | ); 32 | 33 | expect(wrappingDivRef.children, hasLength(4)); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /test/react_suspense_lazy_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:react/react.dart' as react; 2 | 3 | var SimpleFunctionComponent = react.registerFunctionComponent(SimpleComponent, displayName: 'simple'); 4 | 5 | SimpleComponent(Map props) { 6 | return react.div({'id': 'simple-component'}, []); 7 | } 8 | -------------------------------------------------------------------------------- /test/react_suspense_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react_test_utils_test; 4 | 5 | import 'dart:html'; 6 | 7 | import 'package:js/js.dart'; 8 | import 'package:react/react.dart' as react; 9 | import 'package:react/react_dom.dart' as react_dom; 10 | import 'package:test/test.dart'; 11 | 12 | import './react_suspense_lazy_component.dart' deferred as simple; 13 | 14 | main() { 15 | group('Suspense', () { 16 | test('renders fallback UI first followed by the real component', () async { 17 | final lazyComponent = react.lazy(() async { 18 | await simple.loadLibrary(); 19 | await Future.delayed(Duration(seconds: 1)); 20 | return simple.SimpleFunctionComponent; 21 | }); 22 | var wrappingDivRef; 23 | final mountElement = Element.div(); 24 | react_dom.render( 25 | react.div({ 26 | 'ref': (ref) { 27 | wrappingDivRef = ref; 28 | } 29 | }, [ 30 | react.Suspense({ 31 | 'fallback': react.span({'id': 'loading'}, 'Loading...') 32 | }, [ 33 | lazyComponent({}) 34 | ]) 35 | ]), 36 | mountElement, 37 | ); 38 | 39 | expect(wrappingDivRef.querySelector('#loading'), isNotNull, 40 | reason: 'It should be showing the fallback UI for 2 seconds.'); 41 | expect(wrappingDivRef.querySelector('#simple-component'), isNull, 42 | reason: 'This component should not be present yet.'); 43 | await Future.delayed(Duration(seconds: 2)); 44 | expect(wrappingDivRef.querySelector('#simple-component'), isNotNull, 45 | reason: 'This component should be present now.'); 46 | expect(wrappingDivRef.querySelector('#loading'), isNull, reason: 'The loader should have hidden.'); 47 | }); 48 | 49 | test('is instant after the lazy component has been loaded once', () async { 50 | final lazyComponent = react.lazy(() async { 51 | await simple.loadLibrary(); 52 | await Future.delayed(Duration(seconds: 1)); 53 | return simple.SimpleFunctionComponent; 54 | }); 55 | var wrappingDivRef; 56 | var wrappingDivRef2; 57 | final mountElement = Element.div(); 58 | final mountElement2 = Element.div(); 59 | 60 | react_dom.render( 61 | react.div({ 62 | 'ref': (ref) { 63 | wrappingDivRef = ref; 64 | } 65 | }, [ 66 | react.Suspense({ 67 | 'fallback': react.span({'id': 'loading'}, 'Loading...') 68 | }, [ 69 | lazyComponent({}) 70 | ]) 71 | ]), 72 | mountElement, 73 | ); 74 | 75 | expect(wrappingDivRef.querySelector('#loading'), isNotNull, 76 | reason: 'It should be showing the fallback UI for 2 seconds.'); 77 | expect(wrappingDivRef.querySelector('#simple-component'), isNull, 78 | reason: 'This component should not be present yet.'); 79 | await Future.delayed(Duration(seconds: 2)); 80 | expect(wrappingDivRef.querySelector('#simple-component'), isNotNull, 81 | reason: 'This component should be present now.'); 82 | expect(wrappingDivRef.querySelector('#loading'), isNull, reason: 'The loader should have hidden.'); 83 | 84 | // Mounting to a new element should be instant since it was already loaded before 85 | react_dom.render( 86 | react.div({ 87 | 'ref': (ref) { 88 | wrappingDivRef2 = ref; 89 | } 90 | }, [ 91 | react.Suspense({ 92 | 'fallback': react.span({'id': 'loading'}, 'Loading...') 93 | }, [ 94 | lazyComponent({}) 95 | ]) 96 | ]), 97 | mountElement2, 98 | ); 99 | expect(wrappingDivRef2.querySelector('#simple-component'), isNotNull, 100 | reason: 'Its already been loaded, so this should appear instantly.'); 101 | expect(wrappingDivRef2.querySelector('#loading'), isNull, 102 | reason: 'Its already been loaded, so the loading UI shouldn\'t show.'); 103 | }); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /test/react_version_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react_version_test; 4 | 5 | import 'package:js/js.dart'; 6 | import 'package:react/react_client/react_interop.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | // This test helps us verify our test setup, ensuring 11 | // we're running on the React version we expect to be. 12 | 13 | test('Setup check: window.React is React 17', () { 14 | expect(React.version, startsWith('17.')); 15 | }, tags: 'react-17'); 16 | test('Setup check: window.React is React 18', () { 17 | expect(React.version, startsWith('18.')); 18 | }, tags: 'react-18'); 19 | // Then, when running tests: 20 | // - on React 17: `dart run build_runner test -- ... --exclude-tags=react-18` 21 | // - on React 18: `dart run build_runner test -- ... --exclude-tags=react-17` 22 | } 23 | -------------------------------------------------------------------------------- /test/shared_type_tester.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | @JS() 3 | library react_test_utils_test; 4 | 5 | import 'dart:async'; 6 | import 'dart:html' as html; 7 | 8 | import 'package:js/js.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void sharedTypeTests( 12 | void Function(dynamic value) testTypeValue, { 13 | bool skipNormalDartObjects = false, 14 | bool skipDartMaps = false, 15 | bool skipPrimitives = false, 16 | bool skipNumTypes = false, 17 | bool skipFunctions = false, 18 | bool skipBrowserObjects = false, 19 | bool skipLists = false, 20 | bool skipDatetimes = false, 21 | bool skipFutures = false, 22 | bool skipJsAnonInteropTypes = false, 23 | bool skipJsInteropTypes = true, 24 | }) { 25 | if (!skipNormalDartObjects) { 26 | test('normal Dart objects', () { 27 | final object = Foo('f'); 28 | final iterable = Iterable.generate(2); 29 | testTypeValue(object); 30 | testTypeValue(iterable); 31 | }); 32 | } 33 | 34 | if (!skipDartMaps) { 35 | test('Dart maps', () { 36 | final dartMap = {}; 37 | testTypeValue(dartMap); 38 | }); 39 | } 40 | 41 | if (!skipPrimitives) { 42 | group('primitives', () { 43 | test('', () { 44 | const stringValue = 'test'; 45 | const boolValue = false; 46 | const nullValue = null; 47 | 48 | testTypeValue(stringValue); 49 | testTypeValue(boolValue); 50 | testTypeValue(nullValue); 51 | }); 52 | if (!skipNumTypes) { 53 | test('num types', () { 54 | const intValue = 1 as int; // ignore: unnecessary_cast 55 | const doubleValue = 1.1; 56 | const numValue = 1 as num; // ignore: unnecessary_cast 57 | 58 | testTypeValue(intValue); 59 | testTypeValue(doubleValue); 60 | testTypeValue(numValue); 61 | }); 62 | } 63 | }); 64 | } 65 | 66 | if (!skipFunctions) { 67 | test('functions', () { 68 | _functionLocalMethod() {} 69 | final functionLocalMethod = _functionLocalMethod; 70 | // ignore: prefer_function_declarations_over_variables 71 | final functionExpression = () {}; 72 | final functionTearOff = Object().toString; 73 | final functionAllowInterop = allowInterop(() {}); 74 | 75 | testTypeValue(functionLocalMethod); 76 | testTypeValue(functionExpression); 77 | testTypeValue(functionTearOff); 78 | testTypeValue(functionAllowInterop); 79 | }); 80 | } 81 | 82 | if (!skipBrowserObjects) { 83 | test('browser objects', () { 84 | final element = html.DivElement(); 85 | final customEvent = html.CustomEvent('foo'); 86 | final window = html.window; 87 | 88 | testTypeValue(element); 89 | testTypeValue(customEvent); 90 | testTypeValue(window); 91 | }); 92 | } 93 | 94 | if (!skipLists) { 95 | test('lists', () { 96 | final listDynamic = [1, 'foo']; 97 | final listInt = [1, 2, 3]; 98 | 99 | testTypeValue(listDynamic); 100 | testTypeValue(listInt); 101 | }); 102 | } 103 | 104 | if (!skipDatetimes) { 105 | test('datetimes', () { 106 | final dateTime = DateTime.utc(2018); 107 | 108 | testTypeValue(dateTime); 109 | }); 110 | } 111 | 112 | if (!skipFutures) { 113 | test('futures', () { 114 | final future = Future(() {}); 115 | 116 | testTypeValue(future); 117 | }); 118 | } 119 | 120 | if (!skipJsAnonInteropTypes) { 121 | test('JS anonymous interop types', () { 122 | final jsAnonymous = JsTypeAnonymous(); 123 | 124 | testTypeValue(jsAnonymous); 125 | }); 126 | } 127 | 128 | if (!skipJsInteropTypes) { 129 | test('JS interop types', () { 130 | final jsType = JsType(); 131 | 132 | testTypeValue(jsType); 133 | }); 134 | } 135 | } 136 | 137 | class Foo { 138 | final foo; 139 | Foo(this.foo); 140 | } 141 | 142 | @JS() 143 | @anonymous 144 | class JsTypeAnonymous { 145 | external factory JsTypeAnonymous(); 146 | } 147 | 148 | @JS() 149 | class JsType { 150 | external JsType(); 151 | } 152 | -------------------------------------------------------------------------------- /test/templates/html_template-react17.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{testName}} Test 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 45 | {{testScript}} 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/templates/html_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{testName}} Test 8 | 9 | 10 | 11 | 12 | 13 | 16 | 44 | 45 | {{testScript}} 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/test_components.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'package:react/react.dart'; 3 | import 'package:react/react_client/component_factory.dart'; 4 | 5 | /// Base component for event handling classes used in test cases. 6 | class EventComponent extends Component { 7 | @override 8 | getInitialState() => {'text': ''}; 9 | onEvent(SyntheticEvent e) => setState({'text': '${e.type} ${e.timeStamp}'}); 10 | @override 11 | render() => div({ 12 | 'onAnimationEnd': onEvent, 13 | 'onAnimationIteration': onEvent, 14 | 'onAnimationStart': onEvent, 15 | 'onBlur': onEvent, 16 | 'onChange': onEvent, 17 | 'onClick': onEvent, 18 | 'onCopy': onEvent, 19 | 'onCompositionEnd': onEvent, 20 | 'onCompositionStart': onEvent, 21 | 'onCompositionUpdate': onEvent, 22 | 'onContextMenu': onEvent, 23 | 'onCut': onEvent, 24 | 'onDoubleClick': onEvent, 25 | 'onDrag': onEvent, 26 | 'onDragEnd': onEvent, 27 | 'onDragEnter': onEvent, 28 | 'onDragExit': onEvent, 29 | 'onDragLeave': onEvent, 30 | 'onDragOver': onEvent, 31 | 'onDragStart': onEvent, 32 | 'onDrop': onEvent, 33 | 'onFocus': onEvent, 34 | 'onGotPointerCapture': onEvent, 35 | 'onInput': onEvent, 36 | 'onKeyDown': onEvent, 37 | 'onKeyPress': onEvent, 38 | 'onKeyUp': onEvent, 39 | 'onLostPointerCapture': onEvent, 40 | 'onMouseDown': onEvent, 41 | 'onMouseMove': onEvent, 42 | 'onMouseOut': onEvent, 43 | 'onMouseOver': onEvent, 44 | 'onMouseUp': onEvent, 45 | 'onPaste': onEvent, 46 | 'onPointerCancel': onEvent, 47 | 'onPointerDown': onEvent, 48 | 'onPointerEnter': onEvent, 49 | 'onPointerLeave': onEvent, 50 | 'onPointerMove': onEvent, 51 | 'onPointerOver': onEvent, 52 | 'onPointerOut': onEvent, 53 | 'onPointerUp': onEvent, 54 | 'onScroll': onEvent, 55 | 'onSubmit': onEvent, 56 | 'onTextInput': onEvent, 57 | 'onTouchCancel': onEvent, 58 | 'onTouchEnd': onEvent, 59 | 'onTouchMove': onEvent, 60 | 'onTouchStart': onEvent, 61 | 'onTransitionEnd': onEvent, 62 | 'onWheel': onEvent 63 | }, state['text']); 64 | } 65 | 66 | class SampleComponent extends Component { 67 | @override 68 | render() => div(props, [ 69 | h1({}, 'A header'), 70 | div({'className': 'div1'}, 'First div'), 71 | div({}, 'Second div'), 72 | span({'className': 'span1'}) 73 | ]); 74 | } 75 | 76 | class WrapperComponent extends Component { 77 | @override 78 | render() => div(props, props['children']); 79 | } 80 | 81 | final eventComponent = registerComponent(() => EventComponent()) as ReactDartComponentFactoryProxy; 82 | 83 | final sampleComponent = registerComponent(() => SampleComponent()) as ReactDartComponentFactoryProxy; 84 | 85 | final wrapperComponent = registerComponent(() => WrapperComponent()) as ReactDartComponentFactoryProxy; 86 | -------------------------------------------------------------------------------- /test/test_components2.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use_from_same_package 2 | import 'package:react/react.dart'; 3 | 4 | /// Base component for event handling classes used in test cases. 5 | class EventComponent2 extends Component2 { 6 | @override 7 | get initialState => {'text': ''}; 8 | onEvent(SyntheticEvent e) => setState({'text': '${e.type} ${e.timeStamp}'}); 9 | @override 10 | render() => div({ 11 | 'onAnimationEnd': onEvent, 12 | 'onAnimationIteration': onEvent, 13 | 'onAnimationStart': onEvent, 14 | 'onBlur': onEvent, 15 | 'onChange': onEvent, 16 | 'onClick': onEvent, 17 | 'onCopy': onEvent, 18 | 'onCompositionEnd': onEvent, 19 | 'onCompositionStart': onEvent, 20 | 'onCompositionUpdate': onEvent, 21 | 'onContextMenu': onEvent, 22 | 'onCut': onEvent, 23 | 'onDoubleClick': onEvent, 24 | 'onDrag': onEvent, 25 | 'onDragEnd': onEvent, 26 | 'onDragEnter': onEvent, 27 | 'onDragExit': onEvent, 28 | 'onDragLeave': onEvent, 29 | 'onDragOver': onEvent, 30 | 'onDragStart': onEvent, 31 | 'onDrop': onEvent, 32 | 'onFocus': onEvent, 33 | 'onGotPointerCapture': onEvent, 34 | 'onInput': onEvent, 35 | 'onKeyDown': onEvent, 36 | 'onKeyPress': onEvent, 37 | 'onKeyUp': onEvent, 38 | 'onLostPointerCapture': onEvent, 39 | 'onMouseDown': onEvent, 40 | 'onMouseMove': onEvent, 41 | 'onMouseOut': onEvent, 42 | 'onMouseOver': onEvent, 43 | 'onMouseUp': onEvent, 44 | 'onPaste': onEvent, 45 | 'onPointerCancel': onEvent, 46 | 'onPointerDown': onEvent, 47 | 'onPointerEnter': onEvent, 48 | 'onPointerLeave': onEvent, 49 | 'onPointerMove': onEvent, 50 | 'onPointerOver': onEvent, 51 | 'onPointerOut': onEvent, 52 | 'onPointerUp': onEvent, 53 | 'onScroll': onEvent, 54 | 'onSubmit': onEvent, 55 | 'onTextInput': onEvent, 56 | 'onTouchCancel': onEvent, 57 | 'onTouchEnd': onEvent, 58 | 'onTouchMove': onEvent, 59 | 'onTouchStart': onEvent, 60 | 'onTransitionEnd': onEvent, 61 | 'onWheel': onEvent 62 | }, state['text']); 63 | } 64 | 65 | class SampleComponent2 extends Component2 { 66 | @override 67 | render() => div(props, [ 68 | h1({}, 'A header'), 69 | div({'className': 'div1'}, 'First div'), 70 | div({}, 'Second div'), 71 | span({'className': 'span1'}) 72 | ]); 73 | } 74 | 75 | class WrapperComponent2 extends Component2 { 76 | @override 77 | render() => div(props, props['children']); 78 | } 79 | 80 | final eventComponent = registerComponent2(() => EventComponent2()); 81 | 82 | final sampleComponent = registerComponent2(() => SampleComponent2()); 83 | 84 | final wrapperComponent = registerComponent2(() => WrapperComponent2()); 85 | -------------------------------------------------------------------------------- /test/util_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('browser') 2 | library react.util_test; 3 | 4 | import 'dart:js'; 5 | 6 | import 'package:react/react_client.dart'; 7 | import 'package:react/react_client/react_interop.dart'; 8 | import 'package:test/test.dart'; 9 | import 'package:react/react.dart' as react; 10 | import 'util.dart'; 11 | 12 | // ignore_for_file: deprecated_member_use_from_same_package, invalid_use_of_protected_member 13 | 14 | void main() { 15 | group('util test', () { 16 | test('ReactDartComponentVersion.fromType', () { 17 | expect(ReactDartComponentVersion.fromType(react.div({}).type), isNull); 18 | expect(ReactDartComponentVersion.fromType(JsFoo({}).type), isNull); 19 | expect(ReactDartComponentVersion.fromType(Foo({}).type), ReactDartComponentVersion.component); 20 | expect(ReactDartComponentVersion.fromType(Foo2({}).type), ReactDartComponentVersion.component2); 21 | expect(ReactDartComponentVersion.fromType(FunctionFoo({}).type), ReactDartComponentVersion.component2); 22 | }); 23 | 24 | test('isDartComponent2', () { 25 | expect(isDartComponent2(react.div({})), isFalse); 26 | expect(isDartComponent2(JsFoo({})), isFalse); 27 | expect(isDartComponent2(Foo({})), isFalse); 28 | expect(isDartComponent2(Foo2({})), isTrue); 29 | expect(isDartComponent2(FunctionFoo({})), isTrue); 30 | }); 31 | 32 | test('isDartComponent1', () { 33 | expect(isDartComponent1(react.div({})), isFalse); 34 | expect(isDartComponent1(JsFoo({})), isFalse); 35 | expect(isDartComponent1(Foo({})), isTrue); 36 | expect(isDartComponent1(Foo2({})), isFalse); 37 | expect(isDartComponent1(FunctionFoo({})), isFalse); 38 | }); 39 | 40 | test('isDartComponent', () { 41 | expect(isDartComponent(react.div({})), isFalse); 42 | expect(isDartComponent(JsFoo({})), isFalse); 43 | expect(isDartComponent(Foo({})), isTrue); 44 | expect(isDartComponent(Foo2({})), isTrue); 45 | expect(isDartComponent(FunctionFoo({})), isTrue); 46 | }); 47 | }); 48 | } 49 | 50 | final FunctionFoo = react.registerFunctionComponent(_FunctionFoo); 51 | 52 | _FunctionFoo(Map props) { 53 | return react.div({}); 54 | } 55 | 56 | final Foo = react.registerComponent(() => _Foo()) as ReactDartComponentFactoryProxy; 57 | 58 | class _Foo extends react.Component { 59 | @override 60 | render() => react.div({}); 61 | } 62 | 63 | final Foo2 = react.registerComponent2(() => _Foo2()); 64 | 65 | class _Foo2 extends react.Component2 { 66 | @override 67 | render() => react.div({}); 68 | } 69 | 70 | final JsFoo = ReactJsComponentFactoryProxy(React.createClass(ReactClassConfig( 71 | displayName: 'JsFoo', 72 | render: allowInterop(() => react.div({})), 73 | ))); 74 | -------------------------------------------------------------------------------- /tool/dart_dev/config.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Workiva Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:dart_dev/dart_dev.dart'; 16 | import 'package:glob/glob.dart'; 17 | 18 | final config = { 19 | 'format': FormatTool() 20 | ..formatterArgs = ['--line-length=120'] 21 | ..exclude = [ 22 | // Don't format generated mockito files. 23 | Glob('test/mockito.mocks.dart'), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /tool/delete_dart_2_only_files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # This script deletes files that can only be run in Dart 2 (ones not using null-safety), 5 | # and need to be deleted for analysis and compilation to work in Dart 3. 6 | # 7 | 8 | set -e 9 | 10 | rm test/nullable_callback_detection/non_null_safe_refs.dart 11 | rm test/nullable_callback_detection/nullable_callback_detection_unsound_test.dart -------------------------------------------------------------------------------- /tool/run_consumer_tests.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | 5 | Future main(List args) async { 6 | final parser = ArgParser() 7 | ..addOption('repoName') 8 | ..addOption('orgName') 9 | ..addOption('testCmd') 10 | ..addOption('packageName'); 11 | final parsedArgs = parser.parse(args); 12 | 13 | if (parsedArgs['repoName'] == null) { 14 | throw ProcessException('consumer_tests.dart', parsedArgs.arguments, '--repoName must be provided', -1); 15 | } 16 | 17 | if (parsedArgs['orgName'] == null) { 18 | throw ProcessException('consumer_tests.dart', parsedArgs.arguments, '--orgName must be provided', -1); 19 | } 20 | 21 | if (parsedArgs['testCmd'] == null) { 22 | throw ProcessException('consumer_tests.dart', parsedArgs.arguments, '--testCmd must be provided', -1); 23 | } 24 | 25 | final repoName = parsedArgs['repoName'] as String; 26 | final orgName = parsedArgs['orgName'] as String; 27 | final testCmdStr = parsedArgs['testCmd'] as String; 28 | final packageName = parsedArgs['packageName'] ?? parsedArgs['repoName'] as String; 29 | 30 | final reactGitSha = Process.runSync('git', ['rev-parse', 'HEAD']).stdout; 31 | print('Running $packageName tests while consuming react-dart at $reactGitSha ...\n'); 32 | 33 | final rootDir = Directory.current; 34 | final tmpDir = Directory('consumer_testing_temp'); 35 | if (tmpDir.existsSync()) { 36 | tmpDir.deleteSync(recursive: true); 37 | } 38 | tmpDir.createSync(recursive: true); 39 | Directory.current = tmpDir; 40 | final gitCloneProcess = await Process.start( 41 | 'git', 42 | [ 43 | 'clone', 44 | 'https://github.com/$orgName/$repoName.git', 45 | '--progress', 46 | '--branch', 47 | 'master', 48 | '--depth', 49 | '1', 50 | ], 51 | mode: ProcessStartMode.inheritStdio); 52 | await gitCloneProcess.exitCode; 53 | Directory.current = Directory(repoName); 54 | final repoGitSha = Process.runSync('git', ['rev-parse', 'HEAD']).stdout; 55 | print('Successfully checked out $repoName master at $repoGitSha'); 56 | final dependencyOverrides = ''' 57 | dependency_overrides: 58 | react: 59 | git: 60 | url: https://github.com/cleandart/react-dart.git 61 | ref: $reactGitSha 62 | '''; 63 | 64 | final pubspec = File('pubspec.yaml'); 65 | var pubspecSrc = pubspec.readAsStringSync(); 66 | pubspecSrc = pubspecSrc.replaceFirst('dependencies:', '${dependencyOverrides}dependencies:'); 67 | pubspec.writeAsStringSync(pubspecSrc); 68 | final pubGetProcess = await Process.start('dart', ['pub', 'get'], mode: ProcessStartMode.inheritStdio); 69 | await pubGetProcess.exitCode; 70 | 71 | final testArgs = testCmdStr.split(' '); 72 | final consumerTestProcess = 73 | await Process.start(testArgs[0], [...testArgs..removeAt(0)], mode: ProcessStartMode.inheritStdio); 74 | await consumerTestProcess.exitCode; 75 | 76 | Directory.current = rootDir; 77 | if (tmpDir.existsSync()) { 78 | tmpDir.deleteSync(recursive: true); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | mode: process.env.NODE_ENV, 7 | define: { 8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 9 | }, 10 | build: { 11 | outDir: 'lib/js', 12 | emptyOutDir: false, 13 | sourcemap: process.env.NODE_ENV == 'production', 14 | lib: { 15 | entry: { 16 | react: resolve(__dirname, 'js_src/index.mjs') 17 | }, 18 | name: "ReactBundle", // This doesn't do anything but is required. 19 | fileName: (format, entryName) => `${entryName}.${process.env.NODE_ENV == 'production' ? 'min' : 'dev'}.js`, 20 | formats: ['iife'], 21 | }, 22 | minify: process.env.NODE_ENV == 'production', 23 | rollupOptions: { 24 | // make sure to externalize deps that shouldn't be bundled into your library 25 | external: [ 26 | // `react-redux` requires `redux` but we do not utilize the one section that it is used. 27 | // For reference, react-redux.js uses `redux` only one time ever, in [whenMapDispatchToPropsIsObject] 28 | // and it calls `redux.bindActionCreators`. 29 | // 30 | // This line fakes any `require('redux')` calls, so that webpack will not include all of the `redux` code. 31 | 'redux', 32 | ], 33 | output: { 34 | // Provide global variables to use in the UMD build 35 | // for externalized deps 36 | globals: { 37 | // see comment in `rollupOptions.external` above for `redux`. 38 | redux: 'window.Object', 39 | }, 40 | }, 41 | }, 42 | }, 43 | }) 44 | --------------------------------------------------------------------------------