This is a small Angular library that lets you use React components inside Angular projects.
<react-wrapper [component]="Button" [props]="{ children: 'Hello world!' }"></react-wrapper>
function ReactComponent({ text }) {
return <AngularWrapper component={TextComponent} inputs={{ text }}>
}
npm i @bubblydoo/angular-react
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" />
</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 }" />
</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