A library for building statically-typed React UI components using Dart.
A library for building statically-typed React UI components using Dart.
This library also exposes OverReact Redux, which has its own documentation.
Building Components With Null Safety
Version 5.0.0 introduces support for null safety. Full documentation about building components using null safe over_react is coming soon!
There have been a lot of fantastic improvements in this library recently, all of which require some action on your part if you have existing components built prior to the 3.1.0
release of OverReact. We've done everything we can to make the migrations as painless as possible - with the vast majority of changes being handled by some codemod scripts you can run in your libraries locally. As always, if you encounter issues while working through the migration, you can reach out to us in our gitter chat, or open a new issue.
First, you should upgrade your components to UiComponent2
. Check out the UiComponent2
Migration Guide to learn about the benefits of UiComponent2
, the codemod script you can run, and other updates you may need to make manually.
Once you have migrated your components to UiComponent2
, you're ready to start using the "v3" component boilerplate - which is a massive quality of life improvement for component authors! Check out the Component Boilerplate Migration Guide to learn about the benefits of the new boilerplate, the codemod script you can run, and other updates you may need to make manually.
Prerequisites
Familiarize yourself with React JS
Since OverReact is built atop React JS, we strongly encourage you to gain familiarity with it by reading some React JS tutorials first.
Familiarize yourself with Dart Web applications
If you have never built a Web application in Dart, we strongly encourage you to gain familiarity with the core terminology, tools and boilerplate necessary to serve an application locally using Dart. Dart has fantastic documentation and tutorials to get you started.
Add the over_react
package as a dependency in your pubspec.yaml
.
dependencies:
over_react: ^4.0.0
Enable the OverReact Analyzer Plugin (beta), which has many lints and assists to make authoring OverReact components easier!
Include the native JavaScript react
and react_dom
libraries in your app’s index.html
file,
and add an HTML element with a unique identifier where you’ll mount your OverReact UI component(s).
<html>
<head>
<!-- ... -->
</head>
<body>
<div id="react_mount_point">
// OverReact component render() output will show up here.
</div>
<script src="packages/react/react.js"></script>
<script src="packages/react/react_dom.js"></script>
<!-- NOTE: "index" should correspond to the
name of the `.dart` file that contains your `main()` entrypoint. -->
<script type="application/javascript" defer src="index.dart.js"></script>
</body>
</html>
Note: When serving your application in production, use
packages/react/react_with_react_dom_prod.js
file instead of the un-minifiedreact.js
/react_dom.js
files shown in the example above.
Import the over_react
and react_dom
libraries into index.dart
. Then build some components and mount / render a React tree within the HTML element you created in the previous step by calling react_dom.render()
within the main()
entrypoint of your Dart application.
Be sure to namespace the
react_dom.dart
import asreact_dom
to avoid collisions withUiComponent.render
when creating custom components.
import 'dart:html';
import 'package:over_react/react_dom.dart' as react_dom;
import 'package:over_react/over_react.dart';
// Example of where the `Foo` component might be exported from
import 'package:your_package_name/foo.dart';
main() {
// Mount / render your component/application.
react_dom.render(
Foo()(),
querySelector('#react_mount_point'),
);
}
Run webdev serve
in the root of your Dart project.
Note: If you're not using the latest component boilerplate, you'll have to restart your analysis server in your IDE for the built types to resolve properly after the build completes. Unfortunately, this is a known limitation in the analysis server at this time.
Migrate your components to the latest component boilerplate to never worry about this again!
When running unit tests on code that uses the over_react builder (or any code that imports over_react
),
you must run your tests using the build_runner
package.
Warning: Do not run tests via
pub run build_runner test
in a package while another instance ofbuild_runner
(e.g.pub run build_runner serve
) is running in that same package. This workflow is unsupported by build_runner
Run tests using the build_runner
package, and specify the platform to be a browser platform. Example:
$ pub run build_runner test -- -p chrome test/your_test_file.dart
Below are links to a UI component from our example "Todo App", and its analogous tests that we've written for components we use in . We utilize the utilities found in our over_react_test
library.
If you are not familiar with React JS
Since OverReact is built atop React JS, we strongly encourage you to gain familiarity with it by reading this React JS tutorial first.
The over_react
library functions as an additional "layer" atop the Dart react package
which handles the underlying JS interop that wraps around React JS.
The library strives to maintain a 1:1 relationship with the React JS component class and API. To do that, an OverReact component is comprised of four core pieces that are each wired up via our builder.
UiFactory
is a function that returns a new instance of a
UiComponent2
’s UiProps
class.
UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier
UiProps
instance it returns can be used as a component builder,
or as a typed view into an existing props map.castUiFactory
is necessary to prevent implicit cast analysis warnings before code generation has been run.
UiProps
is a Map
class that adds statically-typed getters and setters for each React component prop.
It can also be invoked as a function, serving as a builder for its analogous component.
mixin FooProps on UiProps {
// ... the props for your component go here
String bar;
bool baz;
List<int> bizzles;
}
To compose props mixin classes, create a class alias that uses UiProps
as the base and mix in multiple props mixins. The generated props implementation will then use it as the base class and implement the generated version of those props mixins.
UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier
mixin FooPropsMixin on UiProps {
String bar;
bool baz;
List<int> bizzles;
}
class FooProps = UiProps with FooPropsMixin, BarPropsMixin;
class FooComponent extends UiComponent2<FooProps> {
// ...
}
The use-case for composing multiple props mixins into a single component props class is typically a component that renders another component, and therefore needs to expose the prop interface of that child component which will get forwarded via addUnconsumedProps
.
Check out an example of props mixin component composition here
UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier
mixin FooProps on UiProps {
String color;
}
class FooComponent extends UiComponent2<FooProps> {
// ...
}
void bar() {
FooProps props = Foo();
props.color = '#66cc00';
print(props.color); // #66cc00
print(props); // {FooProps.color: #66cc00}
}
/// You can also use the factory to create a UiProps instance
/// backed by an existing Map.
void baz() {
Map existingMap = {'FooProps.color': '#0094ff'};
FooProps props = Foo(existingMap);
print(props.color); // #0094ff
}
UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier
mixin FooProps on UiProps {
String color;
}
class FooComponent extends UiComponent2<FooProps> {
ReactElement bar() {
// Create a UiProps instance to serve as a builder
FooProps builder = Foo();
// Set some prop values
builder
..id = 'the_best_foo'
..color = '#ee2724';
// Invoke as a function with the desired children
// to return a new instance of the component.
return builder('child1', 'child2');
}
/// Even better... do it inline! (a.k.a fluent)
ReactElement baz() {
return (Foo()
..id = 'the_best_foo'
..color = 'red'
)(
'child1',
'child2'
);
}
}
See fluent-style component consumption for more examples on builder usage.
UiState
is a Map
class (just like UiProps
) that adds statically-typed getters and setters
for each React component state property.
mixin FooState on UiState {
// ...
}
UiState
is optional, and won’t be used for every component. Check out theUiStatefulComponent
boilerplate for more information.
For guidance on updating to
UiComponent2
fromUiComponent
, check out the UiComponent2 Migration Guide.
UiComponent2
is a subclass of react.Component2
, containing lifecycle methods and rendering logic for components.
class FooComponent extends UiComponent2<FooProps> {
// ...
}
UiProps
, as well as utilities for prop forwarding and CSS class merging.UiStatefulComponent2
flavor augments UiComponent2
behavior with statically-typed state via UiState
.
UiComponent2
class, props
and state
are not just Map
s.
They are instances of UiProps
and UiState
, which means you don’t need String keys to access them!
newProps()
and newState()
are also exposed to conveniently create empty instances of UiProps
and UiState
as needed.typedPropsFactory()
and typedStateFactory()
are also exposed to conveniently create typed props
/ state
objects out of any provided backing map.UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier
mixin FooProps on UiProps {
String color;
Function() onDidActivate;
Function() onDidDeactivate;
}
mixin FooState on UiState {
bool isActive;
}
class FooComponent extends UiStatefulComponent2<FooProps, FooState> {
@override
Map get defaultProps => (newProps()
..color = '#66cc00'
);
@override
Map get initialState => (newState()
..isActive = false
);
@override
void componentDidUpdate(Map prevProps, Map prevState, [dynamic snapshot]) {
var tPrevState = typedStateFactory(prevState);
var tPrevProps = typedPropsFactory(prevProps);
if (state.isActive && !tPrevState.isActive) {
props.onDidActivate?.call();
} else if (!state.isActive && tPrevState.isActive) {
props.onDidDeactivate?.call();
}
}
@override
dynamic render() {
return (Dom.div()
..modifyProps(addUnconsumedDomProps)
..style = {
...newStyleFromProps(props),
'color': props.color,
'fontWeight': state.isActive ? 'bold' : 'normal',
}
)(
(Dom.button()..onClick = _handleButtonClick)('Toggle'),
props.children,
);
}
void _handleButtonClick(SyntheticMouseEvent event) {
setState(newState()
..isActive = !state.isActive
);
}
}
The OverReact analyzer plugin has many lints and assists to make authoring OverReact components easier!
In OverReact, components are consumed by invoking a UiFactory
to return a new UiProps
builder, which is then modified and invoked to build a ReactElement
.
This is done to make "fluent-style" component consumption possible, so that the OverReact consumer experience is very similar to the React JS / "vanilla" react-dart experience.
To demonstrate the similarities, the example below shows a render method for JS, JSX, react-dart, and over_react that will have the exact same HTML markup result.
React JS:
render() {
return React.createElement('div', {className: 'container'},
React.createElement('h1', null, 'Click the button!'),
React.createElement('button', {
id: 'main_button',
onClick: _handleClick
}, 'Click me')
);
}
React JS (JSX):
render() {
return <div className="container">
<h1>Click the button!</h1>
<button
id="main_button"
onClick={_handleClick}
>Click me</button>
</div>;
}
Vanilla react-dart:
render() {
return react.div({'className': 'container'},
react.h1({}, 'Click the button!'),
react.button({
'id': 'main_button',
'onClick': _handleClick
}, 'Click me')
);
}
OverReact:
render() {
return (Dom.div()..className = 'container')(
Dom.h1()('Click the button!'),
(Dom.button()
..id = 'main_button'
..onClick = _handleClick
)('Click me')
);
}
Let’s break down the OverReact fluent-style shown above
render() {
// Create a builder for a <div>,
// add a CSS class name by cascading a typed setter,
// and invoke the builder with the HTML DOM <h1> and <button> children.
return (Dom.div()..className = 'container')(
// Create a builder for an <h1> and invoke it with children.
// No need for wrapping parentheses, since no props are added.
Dom.h1()('Click the button!'),
// Create a builder for a <button>,
(Dom.button()
// add a ubiquitous DOM prop exposed on all components,
// which Dom.button() forwards to its rendered DOM,
..id = 'main_button'
// add another prop,
..onClick = _handleClick
// and finally invoke the builder with children.
)('Click me')
);
}
All react-dart DOM components (react.div
, react.a
, etc.) have a
corresponding Dom
method (Dom.div()
, Dom.a()
, etc.) in OverReact.
ReactElement renderLink() {
return (Dom.a()
..id = 'home_link'
..href = '/home'
)('Home');
}
ReactElement renderResizeHandle() {
return (Dom.div()
..className = 'resize-handle'
..onMouseDown = _startDrag
)();
}
DomProps
builder, which can be used
to render them via our fluent interface
as shown in the examples above.
DomProps
has statically-typed getters and setters for all HTML attribute props.
The domProps()
function is also available to create a new typed Map or a typed view into an existing Map. Useful for manipulating DOM props and adding DOM props to components that don’t forward them directly, or to access a DOM prop from a plain map in a lifecycle method as shown below.
@override
void componentDidUpdate(Map prevProps, Map prevState, [dynamic snapshot]) {
// Say you want to compare the previous / current value of `DomProps.title` here...
final titleChanged = domProps(prevProps).title != props.title;
}
A note on dart_style:
Currently, dart_style (dartfmt) decreases the readability of components built using OverReact's fluent-style. See https://github.com/dart-lang/dart_style/issues/549 for more info.
We're exploring some different ideas to improve automated formatting, but for the time being, we do not recommend using dart_style with OverReact.
However, if you do choose to use dart_style, you can greatly improve its output by using trailing commas in children argument lists:
- dart_style formatting:
return (Button() ..id = 'flip' ..skin = ButtonSkin.vanilla)((Dom.span() ..className = 'flip-container')((Dom.span()..className = 'flipper')( (Dom.span() ..className = 'front-side')((Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_RIGHT)()), (Dom.span() ..className = 'back-side')((Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_LEFT)()))));
- dart_style formatting, when trailing commas are used:
return (Button() ..id = 'flip' ..skin = ButtonSkin.vanilla)( (Dom.span()..className = 'flip-container')( (Dom.span()..className = 'flipper')( (Dom.span()..className = 'front-side')( (Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_RIGHT)(), ), (Dom.span()..className = 'back-side')( (Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_LEFT)(), ), ), ), );
To help ensure your OverReact code is readable and consistent, we've arrived at the following formatting rules.
ALWAYS place the closing builder parent on a new line.
Good:
(Button()
..skin = ButtonSkin.SUCCESS
..isDisabled = true
)('Submit')
Bad:
(Button()
..skin = ButtonSkin.SUCCESS
..isDisabled = true)('Submit')
ALWAYS pass component children on a new line with trailing commas and 2 space indentation.
Good:
Dom.div()(
Dom.span()('nested component'),
)
Dom.div()(
Dom.span()('nested component A'),
Dom.span()('nested component B'),
)
Bad:
// Children are not on a new line; in most cases,
// this makes it difficult to quickly determine nesting.
Dom.div()(Dom.span()('nested component'), Dom.span()('nested component'))
// With nested hierarchies, continuation indents can quickly result
// in a "pyramid of Doom"
Dom.div()(
Dom.ul()(
Dom.li()(
Dom.a()('A link!')
)
)
)
// Omitting trailing commas makes it a pain to rearrange lines
Dom.div()(
Dom.span()('nested component A'),
Dom.span()('nested component B')
)
Dom.div()(
Dom.span()('nested component B') // ugh, need to add a comma here...
Dom.span()('nested component A'),
)
AVOID passing children within lists; lists should only be used when the number/order of the children are dynamic.
Good:
Dom.div()(
Dom.span()('nested component'),
Dom.span()('nested component'),
)
var children = [
Dom.div()('List of Items:'),
]..addAll(props.items.map(renderItem));
return Dom.div()(children)
Bad:
Dom.div()([
(Dom.span()..key = 'span1')('nested component'),
(Dom.span()..key = 'span2')('nested component'),
])
AVOID specifying more than one cascading prop setter on the same line.
Good:
(Dom.div()
..id = 'my_div'
..className = 'my-class'
)()
Bad:
(Dom.div()..id = 'my_div'..className = 'my-class')()
Now that we’ve gone over how to use the over_react
package in your project,
the anatomy of a component and the DOM components
that you get for free from OverReact, you're ready to start building your own custom React UI components.
Fill in your props and rendering/lifecycle logic.
Consume your component with the fluent interface.
Run the app you’ve set up to consume over_react
$ webdev serve
That’s it! Code will be automatically generated on the fly by the builder!
Check out some custom component demos to get a feel for what’s possible!
import 'package:over_react/over_react.dart';
part 'foo_component.over_react.g.dart';
UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier
mixin FooProps on UiProps {
// Props go here, declared as fields:
bool isDisabled;
Iterable<String> items;
}
class FooComponent extends UiComponent2<FooProps> {
@override
Map get defaultProps => (newProps()
// Cascade default props here
..isDisabled = false
..items = []
);
@override
dynamic render() {
// Return the rendered component contents here.
// The `props` variable is typed; no need for string keys!
}
}
import 'package:over_react/over_react.dart';
part 'foo_component.over_react.g.dart';
UiFactory<BarProps> Bar = castUiFactory(_$Bar); // ignore: undefined_identifier
mixin BarProps on UiProps {
// Props go here, declared as fields:
bool isDisabled;
Iterable<String> items;
}
mixin BarState on UiState {
// State goes here, declared as fields:
bool isShown;
}
class BarComponent extends UiStatefulComponent2<BarProps, BarState> {
@override
Map get defaultProps => (newProps()
// Cascade default props here
..isDisabled = false
..items = []
);
@override
Map get initialState => (newState()
// Cascade initial state here
..isShown = true
);
@override
dynamic render() {
// Return the rendered component contents here.
// The `props` variable is typed; no need for string keys!
}
}
import 'package:over_react/over_react.dart';
part 'foo_component.over_react.g.dart';
UiFactory<FooProps> Foo = uiFunction(
(props) {
// Set default props using null-aware operators.
final isDisabled = props.isDisabled ?? false;
final items = props.items ?? [];
// Return the rendered component contents here.
// The `props` variable is typed; no need for string keys!
return Fragment()(
Dom.div()(items),
(Dom.button()..disabled = isDisabled)('Click me!'),
);
},
// The generated props config will match the factory name.
_$FooConfig, // ignore: undefined_identifier
);
mixin FooProps on UiProps {
// Props go here, declared as fields:
bool isDisabled;
Iterable<String> items;
}
ALWAYS write informative comments for your component factories. Include what the component relates to, relies on, or if it extends another component.
Good:
/// Use the `DropdownButton` component to render a button
/// that controls the visibility of a child [DropdownMenu].
///
/// * Related to [Button].
/// * Extends [DropdownTrigger].
/// * Similar to [SplitButton].
///
/// See: <https://link-to-any-relevant-documentation>.
UiFactory<DropdownButtonProps> DropdownButton = castUiFactory(_$DropdownButton); // ignore: undefined_identifier
Bad:
/// Component Factory for a dropdown button component.
UiFactory<DropdownButtonProps> DropdownButton = castUiFactory(_$DropdownButton); // ignore: undefined_identifier
ALWAYS set a default / initial value for boolean props
/ state
fields,
and document that value in a comment.
Why? Without default prop values for bool fields, they could be
null
- which is extremely confusing and can lead to a lot of
unnecessary null-checking in your business logic.
Good:
mixin DropdownButtonProps on UiProps {
/// Whether the [DropdownButton] appears disabled.
///
/// Default: `false`
bool isDisabled;
/// Whether the [DropdownButton]'s child [DropdownMenu] is open
/// when the component is first mounted.
///
/// Determines the initial value of [DropdownButtonState.isOpen].
///
/// Default: `false`
bool initiallyOpen;
}
mixin DropdownButtonState on UiState {
/// Whether the [DropdownButton]'s child [DropdownMenu] is open.
///
/// Initial: [DropdownButtonProps.initiallyOpen]
bool isOpen;
}
DropdownButtonComponent
extends UiStatefulComponent2<DropdownButtonProps, DropdownButtonState> {
@override
Map get defaultProps => (newProps()
..isDisabled = false
..initiallyOpen = false
);
@override
Map get initialState => (newState()
..isOpen = props.initiallyOpen
);
}
Bad:
mixin DropdownButtonProps on UiProps {
bool isDisabled;
bool initiallyOpen;
}
mixin DropdownButtonState on UiState {
bool isOpen;
}
DropdownButtonComponent
extends UiStatefulComponent2<DropdownButtonProps, DropdownButtonState> {
// Confusing stuff is gonna happen in here with
// bool props that could be null.
}
AVOID adding props
or state
fields that don't have
an informative comment.
Good:
mixin DropdownButtonProps on UiProps {
/// Whether the [DropdownButton] appears disabled.
///
/// Default: `false`
bool isDisabled;
/// Whether the [DropdownButton]'s child [DropdownMenu] is open
/// when the component is first mounted.
///
/// Determines the initial value of [DropdownButtonState.isOpen].
///
/// Default: `false`
bool initiallyOpen;
}
mixin DropdownButtonState on UiState {
/// Whether the [DropdownButton]'s child [DropdownMenu] is open.
///
/// Initial: [DropdownButtonProps.initiallyOpen]
bool isOpen;
}
Bad:
mixin DropdownButtonProps on UiProps {
bool isDisabled;
bool initiallyOpen;
}
mixin DropdownButtonState on UiState {
bool isOpen;
}
To avoid having to add // ignore: uri_has_not_been_generated
to each
component library on the part/import that references generated code,
ignore this warning globally within analysis_options.yaml:
analyzer:
errors:
uri_has_not_been_generated: ignore
Alternatively, include
workiva_analysis_options
which ignores this warning by default.
Yes please! (Please read our contributor guidelines first)
The over_react
library adheres to Semantic Versioning: