Arez
The combination of Arez and React4j creates a powerful toolkit. Arez manages the application state and React4j transforms the application state into a view. React4j and Arez are based on functional principles and trade higher memory usage for faster execution speed.
React reduces the number of expensive DOM updates through the use of a virtual DOM. The application builds a future view and this is reconciled against the current view and any differences are applied to the DOM.
Arez is built around a data flow graph where state modifications flow through the graph updating nodes only as required. Data nodes are always up to date and perform the minimum amount of work based on the graph definition.
The React4j library bridges the two systems, triggering view updates when the state updates. Changes to observable
data that a @View
component accesses during rendering will schedule the component for re-rendering. The
developer controls the scope of the re-render by controlling the size of the component.
Getting Started
To trigger re-renders of a react4j component when observable Arez state is modified then set the type
parameter of the @View
annotation to TRACKING
(i.e. Annotate the component with
@View( type = View.Type.TRACKING )
). This will result in the @Render
annotated method being invoked
within the scope of a read-only, tracking (Arez) transaction. Changes to the observable state accessed within the
scope of the @Render
annotated method will schedule the component for re-rendering.
Below is a Footer
component extracted from a TodoMVC implementation. It accesses the
observable state AppData.model.totalCount()
, AppData.viewService.getFilterMode()
and
AppData.model.completedCount()
and will re-render each time any of these values change.
@View( type = View.Type.TRACKING )
abstract class Footer
{
@Render
ReactNode render()
{
final int count = AppData.model.totalCount();
final String activeTodoWord = "item" + ( count == 1 ? "" : "s" );
final FilterMode filterMode = AppData.viewService.getFilterMode();
return
footer( new HtmlProps().className( "footer" ),
span( new HtmlProps().className( "todo-count" ),
strong( Integer.toString( count ) ),
text( " " + activeTodoWord + " left" )
),
ul( new HtmlProps().className( "filters" ),
li( a( new AnchorProps()
.className( FilterMode.ALL == filterMode ? "selected" : "" )
.href( "#" ), "All" )
),
li( a( new AnchorProps()
.className( FilterMode.ACTIVE == filterMode ? "selected" : "" )
.href( "#active" ), "Active" )
),
li( a( new AnchorProps()
.className( FilterMode.COMPLETED == filterMode ? "selected" : "" )
.href( "#completed" ), "Completed" )
)
),
AppData.model.completedCount() > 0 ?
button( new BtnProps().className( "clear-completed" ).onClick( e -> AppData.service.clearCompleted() ),
"Clear Completed" ) :
null
);
}
}
Optimizing the component
However this is not the most efficient component. There are several scenarios where the component wil re-render but produce identical output. This is inefficient as React4j will take time to re-render the component to the virtual DOM and then additional time to reconcile the virtual DOM against the actual DOM.
Whether this inefficiency has any impact on the user experience will depend upon the application. In particular it will depend on how frequently the observable data changes, what other parts of the view are updated when the same observable data changes and how dynamic and complex the remainder of the view is. It is often the case that re-rendering the entire component is perfectly fine and will have no impact on the users experience, as in the case with a TodoMVC implementation.
However let's assume that this component needs to be optimized and walk through the steps that would be required to optimize the component to reduce the scope and frequency of re-renders.
Use @Memoize
If you turn on "Highlight Updates" in React's DevTools you will notice that the whole component re-renders any time a Todo is toggled from "complete" to "not complete" or vice-versa. However the html output only changes if the number of completed Todos changes from 0 to not zero or from not zero to zero.
To eliminate these unnecessary renders, the simplest approach is to extract the expression
AppData.model.completedCount() > 0
into a separate @Memoize
method. The @Render
annotated method will only
be scheduled to render if the value returned from the @Memoize
method changes.
This method will look like:
@Memoize
boolean hasCompletedItems()
{
return AppData.model.completedCount() > 0;
}
Using computed properties is one of the easiest and least intrusive mechanisms for optimizing components.
Extract Components
If we return to React's DevTools and turn "Highlight Updates" on again. The next thing you will notice is that the
component is re-rendered any time a Todo is added or removed as the value for the expression
AppData.model.totalCount()
changes. Unfortunately @Memoize
will not help us here as the html output changes
every time a re-render occurs. However we can decide to limit the scope of the rendering by extracting a component
that encapsulates the html that changes.
@View( type = View.Type.TRACKING )
abstract class FooterTodoCount
{
@Render
ReactNode render()
{
final int count = AppData.model.totalCount();
final String activeTodoWord = "item" + ( count == 1 ? "" : "s" );
return
span( new HtmlProps().className( "todo-count" ),
strong( Integer.toString( count ) ),
text( " " + activeTodoWord + " left" )
);
}
}
This component can be rendered via an expression such as FooterTodoCountBuilder.build()
.
The FooterTodoCount
component will still be re-rendered every time a Todo is added or removed but the scope
of the re-render is much smaller and thus the amount of work that React4j has to do is much smaller.
We could also extract another component to manage the links and only re-render this new component when the
filterMode
observable property changes but we have decided against this as it is a relatively infrequent event.
The final Footer
component looks something like:
@View( type = View.Type.TRACKING )
abstract class Footer
{
@Nullable
@Render
ReactNode render()
{
final FilterMode filterMode = AppData.viewService.getFilterMode();
return
footer( new HtmlProps().className( "footer" ),
FooterTodoCountBuilder.build(),
ul( new HtmlProps().className( "filters" ),
li( a( new AnchorProps()
.className( FilterMode.ALL == filterMode ? "selected" : "" )
.href( "#" ), "All" )
),
li( a( new AnchorProps()
.className( FilterMode.ACTIVE == filterMode ? "selected" : "" )
.href( "#active" ), "Active" )
),
li( a( new AnchorProps()
.className( FilterMode.COMPLETED == filterMode ? "selected" : "" )
.href( "#completed" ), "Completed" )
)
),
hasCompletedItems() ?
button( new BtnProps().className( "clear-completed" ).onClick( e -> AppData.service.clearCompleted() ),
"Clear Completed" ) :
null
);
}
@Memoize
boolean hasCompletedItems()
{
return AppData.model.completedCount() > 0;
}
}
On Optimizing
Ultimately measuring performance and optimizing when needed to keep within your performance budget is the ideal goal. It is important to know which parts of your application need to be fast and which parts are less important to optimize. In some cases, the application is small enough to never need optimization while in others optimizing components by default may be a good option (i.e. if the cost of optimization is lower than the cost of determining which parts of the application to optimize).
Best Practices
Arez and React4j is such a powerful combination that many of the best practices that you use when building a React4j application no longer make sense after you integrate Arez. However this section will try to give some helpful suggestions that simplify your development experience. You should also checkout the Arez FAQ Section.
UI state should be modelled with Arez Observable State
Often applications start by just modelling the application domain classes as observable state. So an application
has observable entities to model Employee
, Sale
, Customer
etc. However it is extremely useful to model user
interface state such as which tab is visible, the current application place or route, whether a button is visible
etc. using observable state. This provides a single, unified mechanism for reacting to changes and updating the user
interface.
In an ideal world, we should be able to persist the arez observable state, relaunch the web page and load the observable state and the application should appear just as it was before the relaunch.
The question often arises, when should you use React4j component level state. In most cases this state is no longer necessary however it sometimes makes sense to use component state if the state never needs to be shared with any other component.
Avoid writing "business logic" in your React4j components
In an ideal world your Arez/React4j application, React4j should just be providing the view and Arez should provide the business logic. The business logic method in the Arez components are then called from React4j components. This makes it much easier to reuse, refactor and test the business logic. In most cases the business logic can be tested outside the browser in pure java. Another advantage is that it makes the application much easier to understand.
Separate network interactions from React4j components and other arez business logic components
Network interactions can be notoriously difficult to test. They are business logic and should not be put in your React4j components and instead should be triggered from arez components.
One approach is to extract the service API calls behind an interface and pass the service interface into the constructor of the arez component. This way the unit tests can pass in a mock API service during testing.
Consider the example where you have an action that wants to transition to a view listing employees and wants to load all the employee data for the view. A typical example using the "extract a remote service interface" strategy would be:
@ArezComponent
public abstract class EmployeeService
{
private final RemoteServiceAPI _remote;
private boolean _loading;
private String _employeeData;
private String _errorMessage;
EmployeeService( final RemoteServiceAPI remote )
{
_remote = remote;
}
@Action
public void changeToEmployeeView()
{
setLoading( true );
_remote.loadEmployeeData( data -> {
setLoading( false );
setEmployeeData( data );
}, errorMessage -> {
setLoading( false );
setErrorMessage( errorMessage );
} );
}
@Observable
public boolean isLoading()
{
return _loading;
}
public void setLoading( final boolean loading )
{
_loading = loading;
}
@Observable
public String getEmployeeData()
{
return _employeeData;
}
public void setEmployeeData( final String employeeData )
{
_employeeData = employeeData;
}
@Observable
public String getErrorMessage()
{
return _errorMessage;
}
public void setErrorMessage( final String errorMessage )
{
_errorMessage = errorMessage;
}
}
Another approach that is even easier to test and arguably easier to understand is to have the the @Action
annotated method set state the defines the "intent" to perform a remote service and then have a separate arez
component that uses an @Observe
method that observes the "intent" and performs remote call when the intent
indicates that it is required.
So this results in some minor modifications to the employee service so that the action is implemented as follows:
private boolean _loadEmployeeData;
@Observable( name = "loadEmployeeData" )
public boolean shouldLoadEmployeeData()
{
return _loadEmployeeData;
}
public void setLoadEmployeeData( final boolean loadEmployeeData )
{
_loadEmployeeData = loadEmployeeData;
}
@Action
public void changeToEmployeeView()
{
setLoadEmployeeData( true );
}
Then there would be a separate arez component to observe the intent and perform the remote call:
@ArezComponent
public abstract class EmployeeDataLoader
{
private final RemoteServiceAPI _remote;
@ComponentDependency
final EmployeeService _service;
EmployeeDataLoader( final RemoteServiceAPI remote, final EmployeeService service )
{
_remote = remote;
_service = service;
}
@Observe
void loadEmployeeDataIfRequired()
{
if ( _service.shouldLoadEmployeeData() )
{
_service.setLoading( true );
_remote.loadEmployeeData( data -> {
_service.setLoading( false );
_service.setEmployeeData( data );
_service.setLoadEmployeeData( false );
}, errorMessage -> {
_service.setLoading( false );
_service.setErrorMessage( errorMessage );
_service.setLoadEmployeeData( false );
} );
}
}
}
This approach where you separate intent and have another component that performs the remote call means that your unit tests are much more focused and simpler to understand. It has the disadvantage that it requires more verbose code constructs and can result in more abstraction and indirection. In a small application with a single developer this can have a negative effect. Larger applications with larger teams may benefit from the higher level of abstraction.
Avoid arez annotations other than @Memoize and @Action in React4j components
Following the above best practices, you will find you rarely if ever need to annotate any methods in a
@View
annotated class with any Arez annotations other than @Memoize
and @Action
.