This is part 1 of 3 in a series titled "How do we React?" in which I discuss how Aurelia handles common React scenarios.
The other day Sebastian Markbåge posted the following tweet :
You may have noticed that most other frameworks don't have HoCs, render props or anything like React.Children. These account for a lot the differences between React and other frameworks. How would you solve these use cases if you had to switch to [other framework]?
Great question Sebastian! We're glad you asked. In this blog, we'd like to take some time to begin walking through each of these React capabilities, discussing how Aurelia addresses the same set of scenarios. In this post, we'll cover HoCs. For a discussion of how Aurelia handles render props, please see part 2 or for information on how we handle React.Children, see part 3.
Sidebar
Before Sebastian posed the question, he made the following statement: "You may have noticed that most other frameworks don't have HoCs, render props or anything like React.Children." Some have interpreted this statement negatively and have become offended. I'm going to take Sebastian at face value and assume he means no harm and has an honest curiosity.
That said, I do want to point out that I think this statement isn't quite correct. Most of the frameworks I've worked with, not just for web but also in the native desktop and gaming spaces, have built-in capabilities that address these scenarios. I'm not going to fully defend this assertion here, as it would take considerable space. However, as I walk through Aurelia's approach, I'll point out other technology that has influenced Aurelia, hoping to give our readers some other fun avenues to explore, should they take interest in the broader space of UI frameworks and architectures.
HoCs
The React site defines HoC as follows:
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.
In React practice, a higher-order component involves using a function that creates a new component by wrapping itself around an existing component, applying new behavior as part of the wrapping process. Ultimately, it's a functional technique targeted at enabling reuse and composition.
So, how does Aurelia enable reuse and composition?
Custom Attributes
In Aurelia, we have the ability to create custom HTML attributes which extend an HTML node with new behavior. For example, have a look at this HTML:
<template>
<div interact-draggable.bind="actionOptions"></div>
</template>
In this example, interact-draggable
is a custom attribute that can be attached to any HTML node, including an Aurelia custom element. When added, it enables advanced drag/drop capabilities, utilizing the
interact
library internally.
Creating a custom attribute is as simple as authoring a vanilla JS class, following our naming convention, and implementing appropriate component lifecycle hook methods:
export class InteractDraggableCustomAttribute {
static inject = [Element];
constructor(element) {
this.element = element;
}
attached() {
// Use InteractJS on target element here.
}
}
Here's another example of how we can use custom attributes to take advantage of portals:
<template>
<div>
<input value.bind="filterText" />
<div>
<ul portal>
<li repeat.for="item of items | filter: filterText">${item.name}</li>
</ul>
</template>
In this case, simply applying the portal
attribute to the ul
causes it to be rendered in the portal space, rather than in the current DOM flow.
Aurelia Custom Attributes provide near limitless ways to encapsulate functionality for reuse across your project. Since a custom attribute is just a plain class, it's easy to share with other Aurelia developers too. In fact, you can find the interact attribute and the portal attribute on GitHub.
Influences
This idea is not new and does not originate with Aurelia. In fact, it goes back at least 13 years. That's the first time I used it myself when building the behaviors implementation for my first front-end framework, Caliburn , which was designed for WPF apps. At that time, I used this idea to implement some messaging ideas I liked from Smalltalk, which I re-imagined as attachable binding behaviors in Xaml. Around the same time I was working on that, similar patterns of use began in other areas of the Xaml community and round about 2009 Microsoft released an official implementation known as Expression Blend Behaviors. If you're interested, have a read through this early blog post . I think you'll see some similarities.
This idea is also present in the popular Unity3D game engine. In fact, it's central to the extensibility model of the Unity scene graph and the core enabler of its amazing ecosystem and marketplace. Unity emerged around 2005 and you can read more about its MonoBehavior here .
Dependency Injection
It should be clear by now that Aurelia is a framework that favors an object oriented approach. As such, Aurelia developers are free to leverage all the patterns, practices and tools for OOP composition that have evolved over the last several decades. A particular tool that enables powerful, composition at any level of granularity, is a dependency injection framework. In Aurelia's case, dependency injection is baked deep into its core. You've already seen it above, where we injected the target HTML Element
into a custom attribute, but it can be used for so much more.
Sidebar: What is dependency injection and why should I use it?
As a system increases in complexity, it becomes more and more important to break complex code down into groups of smaller, collaborating functions or objects. However, once we've broken down a problem/solution into smaller pieces, we have then introduced a new problem: how do we put the pieces together?
One approach is to have the controlling function or object directly instantiate all its dependencies. This is tedious, but also introduces the bigger problem of tight coupling and muddies the primary responsibility of the controller by forcing upon it a secondary concern of locating and creating all dependencies. To address these issues, the practice of Inversion of Control (IoC) can be employed. Simply put, the responsibility for locating and/or instantiating collaborators is removed from the controlling function/object and delegated to a 3rd party (the control is inverted). Typically, this means that all dependencies become parameters of the function or object constructor, making every function/object implemented this way not only decoupled but open for extension through providing different implementations of the dependencies. The process of providing these dependencies to the controller is known as Dependency Injection (DI).
Once again, we’re back at our original problem: how do we put all these pieces together? With the control merely inverted and open for injection, we are now stuck having to manually instantiate or locate all dependencies and supply them before calling the function or creating the object, but now we must do this at every function call-site or every place that the object is instanced. It seems as if this may be a bigger maintenance problem than we started with!
Fortunately, there is a battle-tested solution to this problem. We can use a Dependency Injection Container. With a DI container, a class can declare its dependencies, and allow the container to locate the dependencies and provide them to the class. Because the container manages locating and providing dependencies, it can also manage the lifetime of objects, enabling singleton, transient, and object pooling patterns without consumers needing to be aware of these details.
DI in Aurelia
So, how does this play out in Aurelia? Let's say that you have a component that needs to use an HttpClient
to request data, and then in response to that, it wants to ask the Router
to navigate to a particular URL. Also, the original request is parameterized based on the user from the current Session
. Here's what that would look like:
export class MyComponent {
static inject = [Session, HttpClient, Router];
constructor(session, http, router) {
this.session = session;
this.http = http;
this.router = router;
}
async activate() {
const result = await this.http.get(`api/user/${this.session.user.id}`);
if (result.needsPowerfulComposition) {
this.router.navigateToRoute('aurelia');
}
}
}
Here we have a vanilla JS component (HTML view not shown) that has declared its dependency on Session
, HttpClient
and Router
. When MyComponent
is instantiated by the framework, Aurelia will ensure that those dependencies are supplied to the constructor and are ready for use within that component immediately.
This example also shows how Aurelia's async navigation lifecycle methods make it really easy to fetch data and optionally pause rendering until async data is ready. In our next major release of Aurelia, we're planning to extend this capability to standard component lifecycle methods as well.
Influences
Dependency injection frameworks have been around a long time. It's even been used in UI frameworks for quite some time. The original version of Caliburn that I started work on in 2005/2006 leveraged DI internally to enable C# view-models to be composed, similar to above. A year or two later, Microsoft released Prism , which also enabled the use of DI in WPF applications, along with a number of other UI composition patterns.
If you really want to have some fun, you can try and dig up info on Microsoft's Apollo WPF framework (killed before release) and going back even further, check out Microsoft's Composite Application Block (CAB). In my opinion, both are pretty awful, but you can still see some similar scenarios and earlier design evolution, though much less elegant.
Decorators
Decorators aren't something specific to Aurelia at all. It's a very old idea implemented in the syntax of some languages, like Python, and implemented as a pattern in other languages, like C++, Java, and C#. Fortunately for web developers, decorators are coming to JavaScript as a language feature. In the JavaScript incarnation, the decorator construct enables a form of declarative meta-programming for classes. Through a decorator, you can dynamically inherit the decorated class from a generated base class, add methods and properties to the target class, define getters/setters, and do just about anything you can think of. Aurelia leverages decorators to mix in metadata and functionality to a class. For example:
@useShadowDOM
export class MyComponent {
}
Simply adding this decorator "mixes in" the native Shadow DOM behavior to MyComponent
.
Aurelia provides a number of decorators like this, but due to our framework's core design, Aurelia developers can use any vanilla JS decorator with their components to mix in any type of functionality needed. One custom example is the way the Aurelia store plugin uses decorators to help with state management:
import { connectTo } from 'aurelia-store';
@connectTo()
export class App {
}
This decorator connects the App
component to the application's state instance, and handles all subscription to state changes as well as disposal of the listener at the correct time within Aurelia's component lifecycle. Hopefully those of you who have used
Redux
can see how this common React HoC scenario is handled in our case.
Wrapping Up
Custom attributes, dependency injection, and decorators are just a few of the ways that Aurelia enables the same reuse and composition scenarios that a React HoC does. We feel that these techniques enable a nice combination of control with the proper granularity for re-mixing behavior, making complex scenarios simple and maintainable. Of course, if you really like HoCs, you can create Aurelia HoCs too . We're cool with that.
I hope you've enjoyed seeing how Aurelia provides its own approaches to the scenarios typically handled by a React HoC. If you did, check out Part 2 of this series, where we discuss Aurelia's approach to React render props. I think you're really going to enjoy it.