OverReact components are declared using a set of "boilerplate" declarations which are
picked up by the over_react
builder.
This builder generates code, wiring up the different pieces of component declarations and creating typed getters/setters for props and state.
This allows boilerplate to be significantly reduced, making the code you write much cleaner and more grokkable.
Check out some component demos to see the builder in action.
Be sure to familiarize yourself with the OverReact library first.
The builder wires up your factory, props class, and component class so that you don't have to!
All of this generation happens "under the hood", but you can see it in action for your own components by viewing the generated .over_react.g.dart file.
-
The builder parses files and looks for things to generate. Let's start with this component:
UiFactory<FooProps> Foo = castUiFactory(_$Foo); // undefined_identifier mixin FooProps on UiProps { int foo; } class FooComponent extends UiComponent2<FooProps> { @override render() { // ... } }
In parsing/members_from_ast.dart, the builder identifies for top-level declarations that could be part of a component declaration:
UiFactory
variables- Mixins that are
on UiProps
and whose names end withProps
orPropsMixin
(same thing for state) - Classes that extend directly from
UiProps
and whose names end withProps
- Classes whose names end with
Component
Then, in, parsing/declarations_from_members.dart, it uses the names of these declarations to group them together, and determines whether those groups are actually an over_react declaration that needs code generation (vs unrelated code that happens to look similar).
In this case:
- The builder identifies the compilation unit members
Foo
,FooProps
,FooComponent
, which all look like pieces of over_react boilerplate. - The builder sees that
FooProps
meets the requirements for declaring a props mixin, and thus can confidently proceed with code generation for it.- Props mixins need code generation regardless of whether they're associated with a component, since they can be declared on their own.
- The builder starts with the factory, and looks for other members with matching names that it could use to create a complete group that forms a class component. It finds all the pieces it needs:
- a factory: Foo
- either a props mixin or a concrete props class: FooProps
- either a state mixin or a concrete state class (optional)
- a component class: FooComponent
- The builder checks to see if the group is really an over_react declaration that needs code generation. It sees that the factory is referencing a generated variable (
Foo = _$Foo
) and thus can confidently say that the factory needs code generation. Because of that, it proceeds with code generation for all the members in this group.
The builder has recognized our code as a class component that requires code generation! If it hadn't, the builder would have gone through similar logic to identify other types of declarations like function components and props map views, and perform similar code generation to wire them up.
Next up, let's see what code generation looks like for our class component.
-
The builder generates a version of the props mixin which contains concrete getters/setters.
mixin $FooProps on FooProps { int get foo => props['FooProps.foo']; set foo(int value) => props['FooProps.foo'] = value; }
-
The builder creates a concrete props implementation class, which mixes in the generated props mixin:
class _$$FooProps extends UiProps with FooProps, $FooProps { ... }
This concrete class is what will be used everywhere for this component's typed props (returned from the
UiFactory
, available viaprops
insiderender
).Note that in the above example, our component has props declared via just a props mixin, which is a shorthand syntax for when no additional props are needed from other mixins. For components with concrete props classes which mix in other props, the implementation is similar. The generated concrete props class mixes in all generated mixins corresponding to the props mixins used:
// Authored code: UiFactory<BarProps> Bar = castUiFactory(_$Bar); class BarProps = UiProps with BarPropsMixin, FooProps; class BarComponent extends UiComponent2<BarProps> { ... } // Generated code: class _$$BarProps extends UiProps with BarPropsMixin, $BarPropsMixin, FooProps, $FooProps implements BarProps { ... }
It also overrides stubbed out
UiProps
members, and implements a constructor that delegates to other generated subclasses: one optimized forJsBackedMap
, and one used for all otherMap
s.class _$$FooProps extends UiProps with FooProps, $FooProps { factory _$$FooProps(Map backingMap) { if (backingMap == null || backingMap is JsBackedMap) { return _$$FooProps$JsMap(backingMap); } else { return _$$FooProps$PlainMap(backingMap); } } /// The backing props map proxied by this class. @override Map get props => _props; final Map _props; /// Let [UiProps] internals know that this class has been generated. @override bool get $isClassGenerated => true; /// The `ReactComponentFactory` associated with the component built by this class. @override ReactComponentFactoryProxy get componentFactory => $FooComponentFactory; }
Note that the
typedPropsFactory
variable is wired up to use the props implementation class's constructor. This lets us get an instance of that class when we use theprops
getter and thenewProps()
method, and is necessary since the code we wrote doesn't have a proper constructor. -
Our fully implemented component class is registered with the react-dart wrapper.
// React component factory implementation. // // Registers component implementation and links type meta to builder factory. final $FooComponentFactory = registerComponent2( () => new _$FooComponent(), builderFactory: Foo, componentClass: FooComponent, isWrapper: false, parentType: null, displayName: 'Foo', );
-
Finally, the initializer for our factory (
_$Foo
) is generated with a function that returns a new instance of our private props implementation. This factory is the entry-point to externally consuming our component and props class._$$FooProps _$Foo([Map backingProps]) => backingProps == null ? _$$FooProps$JsMap(JsBackedMap()) : _$$FooProps(backingProps);
Writing a statically-typed prop / state API for React components in Dart proved to be a huge pain when we attempted to do it from scratch.
We found that the most straightforward way to adhere to React patterns was to use Map
s for props
and state
.
So we went about defining typed getters and setters on MapView
-like classes which proxy Map key-value pairs.
However, typing these getters and setters quickly became tedious
(especially if you need namespaced keys for platform scalability):
class FooProps extends UiProps {
String get title => props['FooProps.title'];
set title(String value) => props['FooProps.title'] = value;
bool get isEnabled => props['FooProps.isEnabled'];
set isEnabled(bool value) => props['FooProps.isEnabled'] = value;
MouseEventCallback get onClick => props['FooProps.onClick'];
set onClick(Callback value) => props['FooProps.onClick'] = value;
// Imagine a component with 20 of these... yuck.
}
With the builder, we cut down on repetition while preserving statically-typed getter and setters.
Props and state are declared using fields, making the actual code you write much simpler - and much more like the React JS library intended:
mixin FooProps on UiProps {
String title;
bool isEnabled;
MouseEventCallback onClick;
// ...
}
mixin FooState on UiState {
bool isShown;
String currentText;
// ...
}
Check out some component demos for more info about using fields to define props and state values in the real world.