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 }}>
}
npm i @bubblydoo/angular-react
import { AngularReactModule } from '@bubblydoo/angular-react'
@NgModule({
...,
imports: [
...,
AngularReactModule
]
})
Use this component when you want to use React in Angular.
It takes two inputs:
component
: A React componentprops?
: 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
}
Use this component when you want to use Angular in React.
It takes a few inputs:
component
: An Angular componentinputs?
: The inputs you want to pass to the Angular component, in an objectoutputs?
: The outputs you want to pass to the Angular component, in an objectevents?
: The events from the Angular component to listen to, usingaddEventListener
. Event handlers are wrapped inNgZone.run
ref?
: The ref to the rendered DOM element (usesReact.forwardRef
)
import { TextComponent } from './text/text.component'
function Text(props) {
return (
<AngularWrapper
component={TextComponent}
inputs={{ text: props.text }}
events={{ click: () => console.log('clicked') }}/>
)
}
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)
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"}</>
}
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.
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!'
}
}
@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>
)
}
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.
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
}
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"
}
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
}
}
See this blog post for the motivation and more details: Transitioning from Angular to React, without starting from scratch