This is an experimental feature, currently available on the branch Bahmni-IPD-master of openmrs-module-bahmniapps. Some details may change over time, but the core strategy would remain the same
The problem
As Bahmni evolves, we have started building modules in React instead of angularJs. Currently, these react components are built inside angularJs applications using adapters. What this means is that every React based module needs to be built with an angularJs based repository, adding to code duplication and maintenance overheads.
Additionally, any React module which needs to install its components within existing angularJs modules of bahmniapps (e.g. the form-controls) will need to be build along with openmrs-module-bahmniapps
since angularJs does not allow for dynamic dependencies. What this means is, that the bahmniapps project must be rebuilt for every deployment of such a react module.
The solution
WebPack supports a technique called Module Federation, through which it allows the use of dynamic javascript modules which can be fetched at runtime. Coupled with React’s Suspense model, a React component can now use a component loaded from a remote javascript file at runtime. With this strategy, openmrs-module-bahmniapps
can use react components served from a different web-server/container at runtime and these react components can reside in their own repositories with independent deployment strategies. The host UI of openmrs-module-bahmniapps
does not need to be rebuild to get the latest of these react components.
This solution also involves separating out the angularJs adapter code into the openmrs-module-bahmniapps
repository so that these dynamic react modules do not need to be build with any sort of angularjs dependencies and can be set up as pure React projects.
Architecture diagram
Overall flow
openmrs-module-bahmniapps
now contains a separate folder for the react micro-frontend source called/micro-frontends
. Built files from this folder are written toui/app/micro-frontends-dist/
folder so that the angularJs applications can refer to them. These files are typically*.min.js
and*.min.css
.Common code across all MFEs is handled as a unique entry point called
shared
which results inshared.min.js
andshared.min.css
files. This, along with react + react-dom files, needs to be loaded before loading any micro-frontend.A local MFE is available called
next-ui
. This is meant to house react components which don’t need to have their own repositories. angularJs adapters over the main entry components results in an angularJs module calledbahmni.mfe.nextUi
which can be added as dependency to your angularJs module when you includenext-ui.min.*
files in your angualJs entry. Since these components don’t have remotes, they will need to be built and updated along sideopenmrs-module-bahmniapps
repository.For remote MFEs, there is one repository each. These repositories will have exposed certain entry components through the ModuleFederationPlugin. Once these repositories are built and deployed, they will be serving a
remoteEntry.js
file which will be used for loading these remote components. Most likely, this web server would proxied within the Bahmni proxy.At
bahmniapps/micro-frontends/
there will be one WebPack entry point for every remote micro-frontend.This will house angular adapter code for react components resulting in an angular module called
bahmni.mfe.<name-of-entry>
. This needs to be injected into the Bahmni module requiring this micro-frontend.The adapted react components will each lazily load corresponding remote react components using
remotEntry.js
at run time.These will be built as
<name-of-entry>.min.js
and<name-of-entry>.min.css
which can be loaded by the Bahmni module requiring this micro-frontend.
Tools & Techniques used
ModuleFederationPlugin which allows WebPack to reference remote modules
Suspense techniques in react which allows lazy loading of remote react components
react2angular which adapts react components into angularJs components
Decisions & tradeoffs
Node version v14.21.3 (LTS). This is the minimum required which worked with the WebPack features needed to get this running
React version v16.14.0. Existing usage was found for this and did not want to break things. This version supports all the tools needed for Module federation so locked this in.
React is a bit finicky with module federation, especially with hooks around, so the above versions of node and react are completely locked in and need to be in sync with the different repositories for this to work
Carbon components v10.19 and carbon-components-react v7.25. These are basically held back because of our node version. The impact is that the carbon design system documentation would be a bit different since they have changed their sass structure a lot and devs might have trouble mapping the styles correctly. Functionally, not a lot has changed so they should be able to get the right changes from older documentation.
No big changes to the styling solution. Since Carbon design system has Sass, we will continue to work with the same. The micro-frontends do support module sass for isolated styling, but that is about it.
For translations, each remote MFE handles their own translations. We could potentially look at sharing core translations with MFEs later. The local MFE gets to use the core set of translations
The MFEs as of now don’t have an innate concept of configuration. They are free to call the configuration APIs on their own and/or else be passed necessary configurations from the host application during runtime.
The React codebase including the MFEs need to rely on a
window.React
andwindow.ReactDOM
instead of bundling them. This was done so as to not break existing work around form components which rely on similar and the way that angular apps were loading the react bundles. The impact of this is that the WebPack configurations are a bit more involved, obfuscating this dependency and making sure that the actual source code sees no difference in terms of React usage. With this, later migration to a more traditional React bundling model will be painless.
How to write and integrate your MFE
The documentation and steps for this is maintained in the README of openmrs-module-bahmniapps/micro-frontends/README.md
. Here is the current version in Bahmni-IPD-master.
Capabilities & Conventions
Naming conventions for MFE angular modules and components
Each remote MFE needs to have it’s own folder as
micro-frontends/src/<name-of-mfe>/index.js
The webpack entry point for each MFE should be its name
Each remote MFE should be built into it’s own angular module named as
bahmni.mfe.<name>
.Components from an MFE should be named
mfe-<name-of-mfe>-<name-of-component>
.
Following the conventions above, for an example MFE named radiolog
with one React component called Reports
, the files would have the following content (example limited to showcasing the naming conventions)
// micro-frontends/webpack.config.js module.exports = { //... other config entry: { //... other entries radiology: './src/radiology/index.js' } }
// micro-frontends/src/radiology/index.js import { react2angular } from "react2angular"; import { Reports } from "./Reports"; angular.module('bahmni.mfe.radiology'); angular.module('bahmni.mfe.radiology') .component('mfeRadiologyReports', react2angular(Reports));
<!-- in html usage --> <mfe-radiology-reports></mfe-radiology-reports>
Passing data from Host to MFE
The remote MFE can receive data from the host angular application though standard React props. As a matter of convention, it has been decided to wrap these inputs in a single prop object called hostData
. An example usage would look like
// in the controller $scope.hostData = { patient: { // patient info }, otherData: "// other data" }
<!-- in the template --> <mfe-example-component1 host-data="hostData"></mfe-example-component1>
// in the React entry component export function Component1(props) { return <div>{props.hostData.patient.name}</div> }
Note: Remember, angularJs maps camelCase bindings to kebab-case so the hostData
prop will be bound to host-data
binding in the html.
Host reacting to events from MFE
For now, we have decided to rely on the host app passing callbacks to the MFE through a conventional prop named hostApi
which needs to be an object. An example usage would look like
// in the controller $scope.hostApi = { onClose: function() { closeModal(); }, onConfirm: function(id) { service.confirmValues(id); } }
<!-- in the template --> <mfe-example-component1 host-api="hostApi"></mfe-example-component1>
// in the React entry component export function Component1(props) { // *See notes return <button onClick={props.hostApi?.onClose?.()>Close</button> }
Important note regarding hostApi
For some reason, the react2angular
adapter take an extra tick to resolve the functions within hostApi
binding. Due to this, even if your propTypes mark the hostApi functions as required, React will throw reference errors for accessing functions in undefined and similar. I recommend using the optional chaining operator (?.
) to get around this