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-reactimport { 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.runref?: 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 storybookIf you want to use your local build in an Angular project, you'll need to build it:
yarn buildThen, 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