This is not a new idea. Aurelia core team member Dwayne Charrington wrote a post on this back in 2015/16: How To Use React.js In Aurelia

This was originally of interest to me because of a project where I wanted to use Office UI Fabric to build the user interface, but I wanted to use Aurelia rather than React as the framework. In the end I gave up and just went with pure React due to time constraints. Unfortunately, due to the developers having a poor understanding of React, the codebase for that project is now a mess. I'm now thinking it could be simplified by building the site structure in Aurelia and the components in React (more on that later).

Also, this gained my attention again recently because of work that Magnus Danielson is doing on an Aurelia version of the fabric components here: au-office-ui. This is something that I tinkered with in the past but gave up on, so I was pleased to see someone picking it up and I will be following the progress of that project.

But, for this article, my focus is on how I can use React within an Aurelia application so that I can pick apart a badly written React application and rebuild it gradually in Aurelia.

Getting Started

To begin with I created a new application in my folder using the aurelia-cli:

    au new --here

I mostly used the defaults, but I prefer TypeScript to Babel. Once setup, I added react/react-dom:

    yarn add react react-dom

I'll need to update the webpack.config.js file also to support tsx. Find the resolve/extensions property and update:

    resolve: {
        extensions: ['.ts', '.js', '.tsx', '.jsx'],
        ...
    },

Next, find the rule for TypeScript and the optional x in the regex:

  { test: /\.ts(x?)$/, loader: "ts-loader" },

And lastly, I need to include jsx support in the tsconfig.json:

    {
        "compilerOptions": {
            ...
            "jsx": "react",
            ...
        },
        ...
    }

Routing

The application that I am trying to fix uses react-router for the navigation. I wanted to swap this to use the Aurelia router for my site structure.

First, let's create the Aurelia view/view model for the home page:

routes/home.html

<template> 
  <h1>Aurelia Home Page</h1>
</template>

routes/home.ts

export default class { }

Then update the app to use the router with this one page

app.html

<template>
  <router-view></router-view>
</template>

app.ts

import { RouterConfiguration, Router } from 'aurelia-router';
import { PLATFORM } from 'aurelia-framework';

export class App {
  message = 'Hello World!';
  router: Router;

  configureRouter(config: RouterConfiguration, router: Router): void {
    this.router = router;
    config.title = 'Aurelia and React';
 
    config.map([
      { route: ['', 'home'], name: 'home', moduleId: PLATFORM.moduleName('routes/home') }
    ]);

  }

}

Now I will want to embed a React component on my home page.

React custom element

I wanted an easy way to add a React component to my Aurelia components so I borrowed heavily from Dwayne's blog post and created a super simple custom element. This is how I wanted to be able to use it:

<template>

  <react component="path/to/tsx"
         props.bind="myProps"></react>

</template>

Here's the first draft of the implementation:

resources/elements/react.ts

import * as ReactDOM from 'react-dom';
import * as React from 'react';
import {
  bindable,
  autoinject,
  noView,
  Container,
  customElement,
} from 'aurelia-framework';

@autoinject
@noView
@customElement("react")
export class ReactComponent {

  @bindable component: string;
  @bindable props: any;

  constructor(
    private element: Element
  ) {  }

  bind() {

    const component = Container.instance.get(this.component);

    const element = React.createElement(component, this.props);

    ReactDOM.render(
      element,
      this.element
    );

  }

}

NOTE: don't forget to add this new custom element to the global resources:

resources/index.ts

import { PLATFORM } from 'aurelia-pal';
import {FrameworkConfiguration} from 'aurelia-framework';

export function configure(config: FrameworkConfiguration) {
  config.globalResources([
    PLATFORM.moduleName("./elements/react")
  ]);
}

To test this, I created a page as a React component:

components/pages/home.tsx

import * as React from 'react';

export class HomePage extends React.Component<void, void> {

 render() {
   return <h1>React Home Page</h1>
 }

}

And lastly, I updated the home route to use this new component:
routes/home.html

<template>

  <h1>Home (aurelia)</h1>

  <react component="components/pages/home"></react>

</template>

But this isn't enough. The Webpack bundling has no idea that the string value of the component attribute is referencing a module in my project. So, it isn't bundled. The result of that is following line in resources/elements/react.ts just returns the string components/pages/home:

const component = Container.instance.get(this.component);

This is solved in other parts of Aurelia (e.g. <require from=""></require>) within the AureliaPlugin in the aurelia-webpack-plugin package. I didn't really want to look at creating my own Webpack plugin to solve this problem (for now) so I manully registered my React components in a feature:

components/index.ts

import { FrameworkConfiguration } from 'aurelia-framework';
import { HomePage } from './pages/home';

export function configure(config: FrameworkConfiguration) {

  config.container.registerSingleton("components/pages/home", () => HomePage);

}

And added that feature

main.ts

  aurelia.use
   .standardConfiguration()
   .feature(PLATFORM.moduleName('resources/index'))
   .feature(PLATFORM.moduleName('components/index'));

Now my React component will correctly resolve in the DI container.

And when I run the project everything looks good:
Image1

The first header there is coming from Aurelia (routes/home.html) and the second header is coming from react (components/pages/home.tsx).

Office UI Fabric

I now have a super simple approach to taking existing React components and adding them into an Aurelia application. My next thought was whether this approach would work at a finer level of using individual Office UI Fabric components.

So I updated the home page to try using a fabric button:

routes/home.html

<template>

  <h1>Aurelia Home</h1>

  <react component="components/pages/home"
         props.bind="homePageProps"></react>

  <react component="office-ui-fabric-react/button"
         props.bind="{text: 'Hello from Office UI Fabric'}"></react>

</template>

And I updated the manual component register to include this component

components/index.ts

import { FrameworkConfiguration } from 'aurelia-framework';
import { HomePage } from './pages/home';
import { Button } from 'office-ui-fabric-react/lib/Button';

export function configure(config: FrameworkConfiguration) {

  config.container.registerSingleton("components/pages/home", () => HomePage);
  config.container.registerSingleton("office-ui-fabric-react/button", () => Button);

}

And that worked:
Image2-1

Lifting state

In Aurelia you can bind your properties to components using two way binding to keep things in sync. In React, you use a technique called lifing state to keep properties update on the parent components.
Let's take a simple example like a text box. We'll use the fabric text box on our page and try to keep a <p> in our Aurelia component updated with the value as the user types.

First I'll add the TextField component to DI:

components/index.ts

import { FrameworkConfiguration } from 'aurelia-framework';
import { HomePage } from './pages/home';
import { Button } from 'office-ui-fabric-react/lib/Button';
import { TextField } from 'office-ui-fabric-react/lib/TextField';

export function configure(config: FrameworkConfiguration) {

  config.container.registerSingleton("components/pages/home", () => HomePage);
  config.container.registerSingleton("office-ui-fabric-react/button", () => Button);
  config.container.registerSingleton("office-ui-fabric-react/textfield", () => TextField);

}

Now I can add the new component to the home route and put a new <p> for the updated value:

routes/home.html

<template>

  <h1>Aurelia Home</h1>

  <react component="components/pages/home"
         props.bind="homePageProps"></react>

  <react component="office-ui-fabric-react/button"
         props.bind="{text: 'Hello from Office UI Fabric'}"></react>

  <react component="office-ui-fabric-react/textfield"
         props.bind="textfieldProps"></react>

  <p>Text field value: ${textfieldValue}</p>

</template>

Then I need to create the textfieldProps and textfieldValue properties which I am binding to and capture the change event:

routes/home.ts

import { ITextFieldProps } from "office-ui-fabric-react/lib/TextField";

export default class {

  textfieldProps: ITextFieldProps = {    
    label: "My Text Field",
    onChange: (event: any) => this.textfieldValue = event.target.value
  };

  textfieldValue: string; 

}

Here's the result:
Image3

Taking it further

A more realistic example of lifting state would be to have one React component affect the properties of another React component. To test this I just swapped the <p> in the preview example with a button where the text is what is typed into the text field. As it turns out, that's really simple:

routes/home.html

<template>

  <h1>Aurelia Home</h1>

  <react component="components/pages/home"
         props.bind="homePageProps"></react>

  <react component="office-ui-fabric-react/button"
         props.bind="{text: 'Hello from Office UI Fabric'}"></react>

  <react component="office-ui-fabric-react/textfield"
         props.bind="textfieldProps"></react>

  <react component="office-ui-fabric-react/button"
         props.bind="{text: textfieldValue}"></react>

</template>

I just have to use the textfieldValue from the previous example when binding the props and the button text updates as I type:

Image4

Bundle Size

A very valid concern with an approach like this the bundle size. We're using two entire javascript frameworks here, so surely we're going to have a huge bundle. Fortunately not. When building for production, and tinkering a bit with chunking in the Webpack config, this is what we get:
Image5
I don't know about you, but I can live with those sizes!

Conclusion

This was just a brief experiment with what's possible while keeping it simple. It will certainly suffice for my refactoring needs.

I think the approach taken by Magnus is really good though and I can't wait to see how that goes. I like the idea of being able to use his package and forget that React is being used.