React and Angular Applications: How to Embed a Component from a React app into an Angular app in real life (Part 2)
A Real Component
In the previous part of this post, we considered the possibility of embedding a React component into an Angular application and found that this is indeed possible. In theory.
Let's look at this using a real component -- the Header -- as an example
The header includes three main parts:
The tabs – Dashboard, Documents, Cash Flows, Fund Offerings, Insights, View Investor IDs.
The tabs are dynamic for each user and are loaded based on a response from an API call.
The content of the User Applications menu isn’t hardcoded either. We get it from the backend API as well.
As you can see, the Header is a complex component that depends on the backend API calls, that, in turn, involves loading and error processing.
The Log Out menu point involves the session management, which is out of control of the Header and belongs to the host application. This means that not only does the host application control the Header, but vice versa - the Header calls the functions of the host application.
And after we turn the Header into a shared component, we’d expect all these functions to be working perfectly in both React and Angular applications.
At this moment, we have the Header already implemented in the Dashboard (React) and in the Portal (Angular) applications. And we’re not going to re-implement it again. As we discussed in the previous part, we cannot embed an Angular component into React app. So, the only way we have is to embed the Header from the Dashboard to the Portal.
Let’s get started. It will be a glorious hunt. We must defeat a few digital beasts before we get to the end.
Application Context
And the first of them is the Application Context.
The Application Context in React is something that contains all we need to get things done. Important values, settings, callbacks. The Application context is accessible from any component and provides whatever the component needs anytime.
All we need to create the Application Context are Context Providers. They do their work on the application initialization, collecting all the data like hardworking ants that drag different things into the anthill.
Once the work is done the Header can call useContext () and get the data from this magic toolbox. Like this:
const { data, isLoading, isError } = useContext(GlobalConfigsContext)
const { userInfo, getIsLoggedin } = useContext(UserInfoContext)
(GlobalConfigProvider and UserInfoProvider prepared all the data and create GlobalConfigsContext and UserInfoContext.)
But two things may prevent using React Context Providers in Angular.
First – not every Context Provider is compatible with Angular. For example, QueryClientProvider won’t work. At least in our case the Angular application refused to compile with QueryClientProvider.
Second – Context Providers are often application-wide and collect too many things needed for many components. Most of them are not needed in the Header. Some of them can be already collected in the Angular application, and we do not want our components to do double work.
And finally, Context Providers can take a big part of the React application code. If we tried to drag all of them into the Angular app, this would be like we embed the entire React app in the Angular application.
Not the result we expected to get.
So, we must be tough and get rid of most the Application Context in the shared Header. And the magic useContext() will stop working. We should use something else to get the data and callbacks we need.
And the only thing we have for passing the application-wide data to the shared component that would work in both Angular and React is the component properties. The Application Context should pass through the properties. This is a boilerplate because we often have to pass a value via a chain of components from parents to children. But we have no choice.
API Calls
The next digital beast in our way is the React tool for calling the backend API – useQuery().
This is a powerful feature that allows processing all states of the API call in an elegant and easy manner.
But it requires the QueryClientProvider to work. And as we mentioned above, it’s not Angular compatible.
Thus, we need something else for API calls.
We can delegate API calls to the application so that the application would gather the backend data and notify the shared component about the states of the API call via the component properties.
Or if we want the component to make the calls for itself, we have to either use some library that is compatible with both Angular and React or fall back to the basic Fetch API.
Routing
The next digital beast is the Routing.
In a Single Page Application, we have nothing but Routing for navigation between different parts of the application. Routing is a part of a framework that allows showing these parts without HTTP calls to the server.
But Angular and React have their own routing systems, incompatible with each other. And a shared component shouldn’t depend on either system.
There are two ways to deal with this problem.
We can fall back on anchors or full HTTP URLs. This eliminates routing completely but requires a browser to reload the entire site every time when we navigate between its parts. That’s not as smooth as routing, but it’s working. At least we have to do this anyway when we navigate between different microservices, because each microservice is a separate application. And we cannot have a smooth routing between applications.
If we still insist on routing (why do not use it where it’s possible?), we have to wrap it into the application functions and pass these functions to the shared component via our beloved properties. This allows the shared component to be unaware and independent of the framework and its routing subsystem. The shared component just calls the function and doesn’t care about what is under the hood.
If not XX, what Can We Use?
There is some good news in this answer. First, we can still use some Context Providers. Only those which are compatible with the Angular and provide only the data needed for the shared components. Like ThemeProvider for styling support.
Second, nothing can prevent us from using some other features like useState(), useEffect() or useMemo(). We can use any feature that meets two conditions: compile for both frameworks and not using the Application Context directly.
Wrappers
Finally, our shared component has to interact with the application. In the first part of this, we created an Angular wrapper that connects the component to the Angular context. This same wrapper is responsible for gathering all the data needed and transferring this data to the component via properties.
But since we disconnected the component from React Application Context, we need another wrapper in React that would do the same. Here’s the code that shows how we prepare properties in the Angular wrapper:
@Component({
selector: 'app-react-header',
template: `
<div #${containerElementName}></div>`,
encapsulation: ViewEncapsulation.None,
})
export class HeaderWrapperComponent implements OnDestroy, AfterViewInit, OnInit {
@ViewChild(containerElementName, { static: false }) containerRef: ElementRef;
…
ngOnDestroy() {
ReactDOM.unmountComponentAtNode(this.containerRef.nativeElement);
}
private loadGlobalConfigs() {
this.http.getGlobalConfigs()
.then((resp) => {
…
});
}
public async render() {
const bannerProps = {
…
};
const getUserAppsProps = {
…
};
const headerProps = getHeaderProps(this.ds, this.ls, bannerProps, getUserAppsProps);
ReactDOM.render(
<HeaderThemeWrapper {...headerProps} />,
this.containerRef.nativeElement);
}
}
HeaderThemeWrapper:
export default (props) => {
const {theme, ...headerProps} = props;
return (
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>
<Header {...headerProps}/>
</ThemeProvider>
</MuiThemeProvider>
);
};
From this, you can see we do use some context providers for getting the styling theme into the component’s context.
getHeaderProps() is a convenient function that creates props based on other objects.
The React counterpart of the Angular wrapper:
const Wrapper = (props) => {
const oktaAuthHook = useOktaAuth()
const { data, isLoading, isError } = useContext(GlobalConfigsContext)
const { pathname } = useLocation()
const env = getEnv()
const {
userInfo, getIsLoggedin, globalLogout,
setIsLogoutTriggeredByApp, getIsKKRUser,
hasDashboard, hasCashFlows, hasFundOfferings, hasInsights
} = useContext(UserInfoContext)
const isLoggedin = getIsLoggedin(userInfo)
const isKKRUser = getIsKKRUser(userInfo)
const getUserAppsProps = useQuery(
['getUserApps', isLoggedin],
() => isLoggedin && getUserApps(),
onetimeQueryOptions()
)
…
const headerProps = {
env,
pathname,
isLoggedin,
isKKRUser,
hasDashboard,
hasCashFlows,
hasFundOfferings,
hasInsights,
userName,
bannerProps,
getUserAppsProps,
userMenuLogout,
navigateToIdentity,
getToken,
ROUTES,
...props
}
return <Header {...headerProps} />
}
export default Wrapper
We don’t have to create context providers here because we can re-use React Application Context.
Conclusion
It's clear this is a major refactoring effort. We have to rip our component out of the living body of the application, with all the wires sticking out, get rid of those React features that cannot be shared and replace them with working alternatives. When we created the Header, we didn't plan for it to be a shared component. We tried to re-use a component that was initially created for React application, and tightly integrated with React context.
But knowing that, we can create components in the future, that are planned to be shared. Components which meant to interact with the outside world via wrappers and properties.
And this is true -- even if we do not use Angular at all. It's true if we plan for components to be re-usable across just React applications because application contexts are different in different applications.
Disclaimer
The views expressed in each blog post are the personal views of each author and do not necessarily reflect the views of KKR. Neither KKR nor the author guarantees the accuracy, adequacy or completeness of information provided in each blog post. No representation or warranty, express or implied, is made or given by or on behalf of KKR, the author or any other person as to the accuracy and completeness or fairness of the information contained in any blog post and no responsibility or liability is accepted for any such information. Nothing contained in each blog post constitutes investment, legal, tax or other advice nor is it to be relied on in making an investment or other decision. Each blog post should not be viewed as a current or past recommendation or a solicitation of an offer to buy or sell any securities or to adopt any investment strategy. The blog posts may contain projections or other forward-looking statements, which are based on beliefs, assumptions and expectations that may change as a result of many possible events or factors. If a change occurs, actual results may vary materially from those expressed in the forward-looking statements. All forward-looking statements speak only as of the date such statements are made, and neither KKR nor each author assumes any duty to update such statements except as required by law.