/angular-react

Use React in Angular and Angular in React, easily

Primary LanguageTypeScriptMIT LicenseMIT

NPM Storybook

React in Angular and Angular in React

This is a small Angular library that lets you use React components inside Angular projects.

<react-wrapper [component]="Button" [props]="{ children: 'Hello world!' }">
function ReactComponent({ text }) {
  return <AngularWrapper component={TextComponent} inputs={{ text }}>
}

Installation

npm i @bubblydoo/angular-react
import { AngularReactModule } from '@bubblydoo/angular-react'

@NgModule({
  ...,
  imports: [
    ...,
    AngularReactModule
  ]
})

Features

ReactWrapperComponent

Use this component when you want to use React in Angular.

It takes two inputs:

  • component: A React component
  • props?: The props you want to pass to the React component

The React component will be first rendered on ngAfterViewInit and rerendered on every ngOnChanges call.

import Button from './button'

@Component({
  template: `<react-wrapper [component]="Button" [props]="{ children: 'Hello world!' }">`
})
class AppComponent {
  Button = Button
}

AngularWrapper

Use this component when you want to use Angular in React.

It takes a few inputs:

  • component: An Angular component
  • inputs?: The inputs you want to pass to the Angular component, in an object
  • outputs?: The outputs you want to pass to the Angular component, in an object
  • events?: The events from the Angular component to listen to, using addEventListener. Event handlers are wrapped in NgZone.run
  • ref?: The ref to the rendered DOM element (uses React.forwardRef)
import { TextComponent } from './text/text.component'

function Text(props) {
  return (
    <AngularWrapper
      component={TextComponent}
      inputs={{ text: props.text }}
      events={{ click: () => console.log('clicked') }}/>
  )
}

useInjected

The Angular Injector is provided on each React component by default using React Context. You can use Angular services and other injectables with it:

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

const authService = useInjected(AuthService)

useObservable

Because consuming observables is so common, we added a helper hook for it:

import { useObservable, useInjected } from '@bubblydoo/angular-react'

function LoginStatus() {
  const authService = useInjected(AuthService)

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

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

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

Global React Context

If you want to have a global React Context, you can register it as follows:

// app.component.ts

constructor(angularReact: AngularReactService) {
  const client = new ApolloClient()
  // equivalent to ({ children }) => <ApolloProvider client={client}>{children}</ApolloProvider>
  angularReact.wrappers.push(({ children }) => React.createElement(ApolloProvider, { client, children }))
}

In this example, we use ApolloProvider to provide a client to each React element. We can then use useQuery in all React components.

This is only needed when your host app is an Angular app. If you're using Angular-in-React, the context will be bridged.

Refs

You can get a ref to the Angular component instance as follows:

import { ComponentRef } from '@angular/core'

const ref = useRef<ComponentRef<any>>()

<AngularWrapper ref={ref} />

To get the component instance, use ref.instance. To get a reference to the Angular component's HTML element, use ref.location.nativeElement.

To forward a ref to a React component, you can simply use the props:

const Message = forwardRef((props, ref) => {
  return <div ref={ref}>{props.message}</div>
})

@Component({
  template: `<react-wrapper [component]="Message" [props]="{ ref, message }">`,
})
export class MessageComponent {
  Message = Message

  message = 'hi!'

  ref(div: HTMLElement) {
    div.innerHTML = 'hi from the callback ref!'
  }
}

Reading React contexts in Angular

@Component({
  selector: "inner",
  template: `number: {{ number$ | async }}`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class InnerComponent {
  number$ = this.contexts.read(NumberContext)

  constructor(@Inject(InjectableReactContextToken) public contexts: InjectableReactContext) {}
}

function App() {
  const [number, setNumber] = useState(42)
  return (
    <NumberContext.Provider value={number}>
      <button onClick={() => setNumber(number + 1)}>increment</button>
      <AngularWrapper component={InnerComponent} />
    </NumberContext.Provider>
  )
}

Using templates

useToAngularTemplateRef: to convert a React component into a TemplateRef

import { useToAngularTemplateRef } from "@bubblydoo/angular-react"

@Component({
  selector: 'message',
  template: `
    <div>
      <ng-container
        [ngTemplateOutlet]="tmpl"
        [ngTemplateOutletContext]="{ message }"
        [ngTemplateOutletInjector]="injector"
      ></ng-container>
    </div>
  `,
})
class MessageComponent {
  @Input() tmpl: TemplateRef<{ message: string }>
  @Input() message: string

  constructor(public injector: Injector) {}
}

function Text(props: { message: string }) {
  return <>{props.message}</>
}

function Message(props: { message: string }) {
  const tmpl = useToAngularTemplateRef(Text)

  const inputs = useMemo(() => ({
    message: props.message,
    tmpl,
  }), [props.message, tmpl])

  return <AngularWrapper component={MessageComponent} inputs={inputs} />
}

Note: useToAngularTemplateRef is meant for usage with [ngTemplateOutletInjector]="injector". If you can't use that, use useToAngularTemplateRefBoundToContextAndPortals instead.

useFromAngularTemplateRef: to convert a TemplateRef into a React component

function Message(props: {
  message: string
  tmpl: TemplateRef<{ message: string }>
}) {
  const Template = useFromAngularTemplateRef(props.tmpl)

  return <Template message={props.message.toUpperCase()} />
}

@Component({
  selector: "outer",
  template: `
    <ng-template #tmpl let-message="message">{{ message }}</ng-template>
    <div>
      <react-wrapper
        [component]="Message"
        [props]="{ tmpl, message }"
      ></react-wrapper>
    </div>
  `,
})
class MessageComponent {
  Message = Message

  @Input() message!: string
}

Developing

You can test the functionality of the components inside a local Storybook:

yarn storybook

If you want to use your local build in an Angular project, you'll need to build it:

yarn build

Then, use yarn link:

cd dist/angular-react
yarn link # this will link @bubblydoo/angular-react to dist/angular-react
# or `npm link`

In your Angular project:

yarn link @bubblydoo/angular-react
# or `npm link @bubblydoo/angular-react`

node_modules/@bubblydoo/angular-react will then be symlinked to dist/angular-react.

You might want to use resolutions or overrides if you run into NG0203 errors.

"resolutions": {
  "@bubblydoo/angular-react": "file:../angular-react/dist/angular-react"
}

Usage notes

this is undefined when passing an Angular component method as a React prop

Angular component methods are always called with the component instance as this. When you pass an Angular method as a prop to a React component, this will be undefined.

@Component({
  template: `<react-wrapper [component]="Button" [props]="{ onClick }">`
})
class AppComponent {
  Button = Button

  onClick() {
    console.log(this) // undefined
  }
}

You can fix it as follows:

@Component({
  template: `<react-wrapper [component]="Button" [props]="{ onClick }">`
})
class AppComponent {
  Button = Button

  onClick = () => {
    console.log(this) // AppComponent instance
  }
}

Further reading

See this blog post for the motivation and more details: Transitioning from Angular to React, without starting from scratch