Transitioning from Angular to React, without starting from scratch
Tags: webdev, react, angular
Published: 2022-05-30 10:28:00

While Angular is a great framework, the engineering effort put in by the React team and community is unmatched in the frontend world. So much so, that we have recently started transitioning from Angular to React.

Some differences are really important to us:
- The latest and greatest stuff is happening in React world
- More community components available
- More developer-friendly API
- Change detection is much easier to wrap your head around
- Smaller API surface, meaning less confusion
- Better choice of primitives (components as variables vs templates with `ng-template` and `ng-content`)
- Clear path towards SSR
- Integrating React Three Fiber

However, we have a perfectly good website that we don't want to throw away. That's why we decided to write a library that allows us to use React in Angular and Angular in React.

That may sound like quite a challenge, but we've figured out a way to do it rather easily (basically by just using `ReactDOM.render`).

I'll share a few extracts from our source code so you can see how we're doing it. If you want to dive right in, check out the library at [github.com/bubblydoo/angular-react](https://github.com/bubblydoo/angular-react).

The basic idea is that there are wrapper components:

```html
<!-- Using a React component in Angular: -->
<react-wrapper
  [component]="Button"
  [props]="{ children: 'Hello world!' }">
</react-wrapper>
```

```tsx
// Using an Angular component in React:
function Text(props) {
  return (
    <AngularWrapper
      component={TextComponent}
      inputs={{ text: props.text }}>
  )
}
```

These wrappers should then render the components using the other framework.

## React wrapper

The react-wrapper is basically implemented as follows:

```tsx
@Component({
  selector: "react-wrapper",
  template: `<div #wrapper></div>`,
})
export class ReactWrapperComponent implements OnChanges, OnDestroy {
  @ViewChild("wrapper") containerRef: ElementRef;
  @Input()
  props: any = {};
  @Input()
  component: React.ElementType;

  ngAfterViewInit() {
    this.render();
  }

  ngOnChanges() {
    this.render();
  }

  private render() {
    ReactDOM.render(
      <this.component {...this.props} />,
      this.containerRef.nativeElement
    );
  }
}
```
You can find its full source code [here](https://github.com/bubblydoo/angular-react/blob/main/projects/angular-react/src/lib/react-wrapper/react-wrapper.component.tsx).

Any time the component or the props change, React will rerender the component. Pretty simple!

This is possible because in React, components are very lightweight, we can easily pass components around and render them. There is no need for any global setup. On the other hand, in Angular, there is a need for a global context. What if we want to pass Angular children into a React component?

## Angular wrapper

Mounting an Angular component inside an arbitrary DOM element is also possible, although not so elegant. We need to have the global `NgModuleRef` and an Angular Component to do so.

Using a callback ref, we mount the Angular Component into a React component.

We pass a div element to the Angular Component as its children, and then use `React.createPortal` to mount React children into it.

```tsx
function AngularWrapper(props) {
  const ngComponent = props.component;
  const ngModuleRef = useContext(AngularModuleContext);

  const hasChildren = !!props.children
  const ngContentContainerEl = useMemo<HTMLDivElement | null>(() => {
    if (hasChildren) return document.createElement('div');
    return null;
  }, [hasChildren]);

  const ref = useCallback<(node: HTMLElement) => void>(
    (node) => {
      const projectableNodes = ngContentContainerEl ? [[ngContentContainerEl]] : [];
      const componentFactory = ngModuleRef.componentFactoryResolver.resolveComponentFactory(ngComponent);
      const componentRef = componentFactory.create(ngModuleRef.injector, projectableNodes, node);
    },
    [ngComponent, ngModuleRef]
  );

  return (
    <>
      {React.createElement(componentName, { ref })}
      {ngContentContainerEl && ReactDOM.createPortal(<>{children}</>, ngContentContainerEl)}
    </>
  );
}
```

There's much more to this file, such as:
- event handling
- inputs & outputs handling
- passing the `React.forwardRef` correctly

You can find the source code [here](https://github.com/bubblydoo/angular-react/blob/main/projects/angular-react/src/lib/angular-wrapper-with-module/angular-wrapper-with-module.tsx)

## Result

We can use Angular and React components interchangeably now!

```tsx

@Component({
  selector: 'inner-angular',
  template: `<div style="border: 1px solid; padding: 5px">this is inner Angular</div>`,
})
class InnerAngularComponent {}

@Component({
  template: `
    <div style="border: 1px solid; padding: 5px">
      <div>this is outer Angular</div>
      <react-wrapper [component]="ReactComponent"></react-wrapper>
    </div>
  `,
})
class OuterAngularComponent {
  ReactComponent = ReactComponent;
}

function ReactComponent(props: { children: any }) {
  return (
    <div style={{ border: '1px solid', padding: '5px' }}>
      <div>this is React</div>
      <div>
        <AngularWrapper component={InnerAngularComponent} />
        {props.children}
      </div>
    </div>
  );
}
```

This renders correctly, as follows:

![React in Angular and Angular in React demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gguymi9y98zgc6ynhpmg.png)

## Using Angular services in React

Ever wondered how the dependency injection in Angular works? Well, it just keeps a simple key-value map internally. This key-value map is the injector.

In the react wrapper, we actually also provide the injector on the context:

```tsx
ReactDOM.render(
  <InjectorContext value={this.injector}>
    <this.component {...this.props} />
  </InjectorContext>,
  this.containerRef.nativeElement
)
```

This way, we can get services as follows in React:

```tsx
const injector = useContext(InjectorContext)
const authService = injector.get(AuthService)
```

We added a shorthand hook to the library to make this even shorter:

```tsx
import { useInjected } from '@bubblydoo/react-angular'

const authService = useInjected(AuthService)
```

This way, it's easy to use Angular injectables in React components!

## Using Observables in React

While there are more feature-complete solutions like [observable-hooks](https://github.com/crimx/observable-hooks), consuming RxJS Observables is so common in Angular that we also added a hook for it.

```tsx
import { useObservable } from '@bubblydoo/react-angular'

const [value, error, completed] = useObservable(authService.isAuthenticated$)

if (error) return <>Something went wrong!<>

return <>{value ? "Logged in!" : "Not logged in"}</>
```

## Example component: `Link` (inspired by Next.js)

This component makes it possible to use the Angular Router in React components.

```tsx
interface Props {
  link?: string[];
  children: any;
}

export default function Link(props: Props) {
  const { link, children } = props;

  const router = useInjected(Router);
  const zone = useInjected(NgZone);
  const activatedRoute = useInjected(ActivatedRoute);
  const locationStrategy = useInjected(LocationStrategy);

  const onClick = (e?: any) => {
    e?.preventDefault();
    zone.run(() => {
      router.navigate(link);
    });
  };

  const urlTree = router.createUrlTree(link, { relativeTo: activatedRoute });
  const href = locationStrategy.prepareExternalUrl(router.serializeUrl(urlTree));
  const childProps = { onClick, href };

  return React.cloneElement(children, childProps);
}
```

Usage:
```tsx
<Link link={['dashboard']}>
  <a>Go to dashboard</a>
</Link>
```

## Where we can improve

### i18n

Angular i18n is not handled very well yet. `$localize` is not working in `.tsx` files. It's also not possible to add `i18n` attributes to React components, because `extract-i18n` only looks at Angular templates.

### NgZone

Working with NgZone: sometimes events coming from a React element should be handled with `ngZone.run(() => ...)`, in case you want to use Angular services and the events are not being tracked by the Zone. See the Link example component above.

### Getting a reference to the Angular component in React

This is not yet implemented either, but we could add a `componentRef` prop to the `AngularWrapper` to get a reference to the Angular component.

### Passing Angular children into `react-wrapper`

Right now this is not possible:

```html
<react-wrapper [component]="Button">
  <div>Angular Text</div>
</react-wrapper>
```

The workaround:

```ts
@Component({
  template: `<div>Angular Text</div>`
})
class TextComponent {}

@Component({
  template: `
<react-wrapper [component]="Button" [props]="{{children}}"></react-wrapper>
`
})
class AppComponent {
  children = React.createElement(
    AngularWrapper,
    { component: TextComponent }
  )
}
```

Adding this would make using the components interchangeably a lot easier.

### Using the AngularWrapper outside of an Angular component

Right now, this library is meant to be used inside Angular projects. More testing should be done to make sure it also works inside Next.js and Create React App projects.

One of the problems is that you would need to create and provide an NgModule:

```tsx
import { AngularModuleContext, AngularWrapper } from '@bubblydoo/angular-react'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'

const ngModuleRef = await platformBrowserDynamic()
  .bootstrapModule(AppModule)

<AngularModuleContext.Provider value={ngModuleRef}>
  <div>
    <AngularWrapper component={Text} inputs={{ text: 'Hi!'}}/>
  </div>
</AngularModuleContext.Provider>
```

Another problem is the build system: Next.js and CRA projects aren't configured to read and bundle Angular code, which needs special compilers. This configuration is usually provided by `@angular/cli`, but it would be hard integrating that with the existing Next.js/CRA build system.

- You would need to setup a separate Angular library project to build the Angular components into normal Javascript code.
- You would need to use `@angular/compiler-cli/linker/babel` as a `babel-loader` plugin in your Webpack config.
- You can then import the Angular components into your React project.

We're considering refining this approach in order to move to Next.js, so we can finally leverage SSG/SSR. We're experimenting with Turborepo in order to manage the separate projects.

## Credits

Although they're very different projects, I mostly took inspiration from [@angular/elements](https://github.com/angular/angular/blob/main/packages/elements/src/component-factory-strategy.ts), and also from [microsoft/angular-react](https://github.com/microsoft/angular-react) (this is a legacy project, that doesn't work for newer Angular versions anymore).