Migrating to Core v11
This guide is for migrating frontend modules to Core v11 and above. v11 has several breaking changes including:
- A major re-write of the extension system internals
- Using Browserslist when compiling to JS
- Not using CSS-in-JS in production builds (RFC-0033)
- Upgrading to SWR 2.5.1 and sharing it through Module Federation
- Removing offline support and rxjs from the framework
- Removing workspace v1
Core v11 is the framework version that ships with Reference Application 3.8.0. Some changes also affect distributions rather than individual modules. See For distribution maintainers.
Background
Core v11 includes these breaking changes as they will improve the overall performance, stability, and correctness of the frontend overall. Even with these changes, we expect most applications to continue working with no modification or very small modifications. This document exists to call out specific uses that are no longer supported.
Migration steps
Bump core dependencies
Bump core dependencies to their newest next tagged versions:
yarn up openmrs@next @openmrs/esm-framework@nextIf this replaces next with concrete prerelease versions in devDependencies, set openmrs and @openmrs/esm-framework back to next and run yarn before committing. Keep any intentional changes and the peer dependency ranges your repo already uses. See Development for more.
Update the framework peer dependency
Update the @openmrs/esm-framework peer dependency in each frontend module’s package.json:
{
"peerDependencies": {
"@openmrs/esm-framework": "11.x"
}
}Share SWR
Make sure swr is a peer dependency of each frontend module, and pin the development copy to the version the app shell provides:
{
"peerDependencies": {
"swr": "2.x"
},
"devDependencies": {
"swr": "2.5.1"
}
}See SWR 2.5.1 for why this matters.
Perform necessary migrations
The necessary migrations are detailed below, depending on use-case
After completing the steps above, verify everything works:
- Run tests:
yarn test - Run a development build:
yarn start - Run a production build:
yarn build
Migration Patterns
Extension system
Assigned Extensions vs Candidate Extensions
The biggest change to the extension system is to the concept of assignedExtensions. Previously, assignedExtensions listed every extension that should be rendered into a slot and was stored as part of the extension store. Unfortunately, since each instance of a slot can have a different state and whether or not an extension should be displayed can depend on that state, it is not tenable to retain that in the store. There are two replacements:
Metadata about extensions
In some cases, the extension store may have been used to get access to extensions simply to get each extension’s associated meta information, for things like dashboard renders. In this case, the correct migration is simply to use the existing useExtensionSlotMeta() hook.
Do something with extensions
The other common use case is to use the assignedExtensions to influence how extensions are displayed. In this case, the useAssignedExtensions(slotName, state) hook or getAssignedExtensions(slotName, state) framework function should be the preferred way of finding assignedExtensions. Pass the state of the slot rendering you’re asking about. These functions don’t load the slot’s state themselves, so without it, display conditions are evaluated against the session alone, which hides extensions whose conditions need values from the slot’s state.
What is in the store
Instead of tracking all assignedExtensions the store now tracks candidateExtensions; candidateExtensions are extensions that are configured to be displayed in a slot before their display conditions have been evaluated against the current state. In some circumstances, e.g., to build up metadata maps or the like, it may be useful to use this since it will generally be faster than the getAssignedExtensions() or useAssignedExtensions() calls—which evaluate the display conditions against the state you pass. However, if you use these, you should be aware that not all candidateExtensions will actually render.
Parcel must mount within 15 seconds
Previously, parcels that did not mount would simply be awaited indefinitely. There is now a hard time-out of 15 seconds after which any parcel that does not mount will be rejected and removed from the application. There is currently no way to configure this feature on a per-extension basis. If your extension takes longer than 15 seconds to mount, re-write it to mount a “Loading” style skeleton faster and defer any loading work.
Extensions added through configuration now retain the order they are defined in configuration
If you’ve configured a slot to add extensions, e.g.,
{
"@openmrs/esm-patient-chart-app": {
"extensionSlots": {
"main-slot": {
"add": [
"custom-extension#1",
"custom-extension#2"
]
}
}
}
}These will now, by default, render in that order but after most extensions bound to that slot. To fix the extension order, update your configuration to define the order:
{
"@openmrs/esm-patient-chart-app": {
"extensionSlots": {
"main-slot": {
"add": [
"custom-extension#1",
"custom-extension#2"
],
"order": [
"custom-extension#2",
"default-extensions",
"custom-extension#1"
]
}
}
}
}For more, see the O3 Configuration overview for details on these mechanisms.
.swcrc with Webpack
Prior to v11, Webpack builds would honor .swcrc if included alongside an app’s webpack.config.js. With v11, that feature has been removed to retain parity with the Rspack configuration, which does not check this file. If you need to add additional configuration, do so using the scriptRuleConfig export. With Webpack, put your SWC options inside use:
const configuration = require('openmrs/default-webpack-config');
configuration.scriptRuleConfig.use = {
options: {
// Your SWC options go here
},
};
module.exports = configuration;The Webpack rule already defines use, so setting scriptRuleConfig.options instead makes the build fail with “A Rule must not have a ‘options’ property when it has a ‘use’ property”. Your options are merged into the default SWC options.
With Rspack, the rule sets the SWC loader’s options directly, so set them there:
const configuration = require('openmrs/default-rspack-config');
configuration.scriptRuleConfig.options = {
// Your SWC options go here
};
module.exports = configuration;CSS migration
As part of migrating away from CSS-in-JS, we are also removing the ability for apps to change global styles. This was always the intention as having different apps apply different global styles quickly makes it hard to track down styling infelicities. For truly global styles that an implementation needs to provide, the openmrs build command has an --asset argument that can be used to provide asset paths that are copied into the final build and rendered as part of the application. Repeat it for each file. For other CSS, the goal should be to ensure that they are always properly scoped. “Properly scoped” means that any rule published by an app is scoped in a way that it doesn’t affect modules outside its tree. For example:
:global(.cds--btn) {
color: blue;
}Affects the color of every Carbon button, but:
.myComponent :global(.cds--btn) {
color: blue;
}Scopes the changes strictly to the class .myComponent, which, with our default setup, will be correctly mangled at run time.
Production builds check this. In Core v11, the Webpack and Rspack configs fail a production build if its CSS has a rule that applies to the whole page: a selector anchored only on a Carbon class, such as :global(.cds--btn), or a selector with no class or id of its own, such as html, body. The error starts with “your module emits n global CSS rule(s)”. Development builds don’t run this check, so run yarn build to find these rules.
Carbon selectors usually reach a module’s CSS through a Carbon component stylesheet imported only for its tokens. Importing the stylesheet emits the component’s CSS too, so import its tokens instead:
// Emits the button's CSS as well as its tokens
@use '@carbon/styles/scss/components/button';
// Tokens only
@use '@carbon/styles/scss/components/button/tokens' as button;One pattern we’ve sometimes seen is using react-to-print and rendering a rule like:
@media print {
html,
body {
margin: 0 !important;
padding: 0 !important;
}
}To try to remove the default padding when printing. For these cases, react-to-print accepts a pageStyle parameter that allows you to define these kinds of global rules and that should be preferred because it is restricted to react-to-print’s iframe and does not affect other rendering. A rule like the one above also fails the production build check, since html and body have no class to scope them, even inside @media print.
SWR 2.5.1
Core v11 upgrades SWR from 2.2.5 to 2.5.1 and shares it through Module Federation, so every frontend module uses the app shell’s copy. Core v10 shared only SWR’s internals, which hold its cache. From SWR 2.3.0, swr loads its internals in a way that this sharing doesn’t catch, so a module that bundles swr keeps its own cache. It no longer sees mutate() calls made by other modules, and the framework’s SWR defaults don’t apply to it.
A module only shares swr if it lists swr in peerDependencies, as shown in Share SWR. If your module already has the peer dependency, bump the development copy:
yarn up --exact swr@2.5.1The SWR APIs O3 modules use, such as useSWR, useSWRInfinite, mutate, isLoading and keepPreviousData, are still available in 2.5.1.
Offline support and rxjs
Core v11 removes offline support from the framework, along with the service worker, the offline tools app and the framework’s dependency on rxjs.
- Remove offline setup. Delete calls to
setupOfflineSync,setupDynamicOfflineDataHandler,subscribePrecacheStaticDependenciesandmessageOmrsServiceWorker, usually found instartupAppor anoffline.tsfile. These are still exported as deprecated no-ops so that older modules keep loading, but they’ll be removed in a future version.useConnectivity()is also kept, and always returnstrue. - Remove the rest of the offline API. Functions such as
queueSynchronizationItem,getSynchronizationItem,syncOfflinePatientData,getOfflinePatientDataStore,generateOfflineUuid,isOfflineUuid,isOnline,getCurrentOfflineModeandsubscribeConnectivityare no longer exported.fetchCurrentPatientno longer takes anincludeOfflinePatientsargument. - Remove
onlineandofflinefromroutes.json. The framework ignores them. - Add rxjs yourself if you still use it. The app shell no longer shares rxjs and the framework no longer depends on it, so a module that uses rxjs must declare it as its own dependency.
Several framework functions now return Promises instead of rxjs Observables:
| Function | Core v10 | Core v11 |
|---|---|---|
getCurrentUser() | Observable that emits each time the session changes | Promise that resolves once |
getLocations() | Observable | Promise<Array<Location>> |
getVisitTypes() | Observable | Promise<Array<VisitType>> |
openmrsObservableFetch() | Observable | Removed. Use openmrsFetch() |
To keep reacting to session changes, use useSession() in React components, or subscribe to the session store. Unlike getCurrentUser() in Core v10, the store’s subscribe only calls you when the session changes, so read the current session with getState() as well:
// Core v10
const subscription = getCurrentUser({ includeAuthStatus: false }).subscribe(setUser);
return () => subscription.unsubscribe();
// Core v11
const store = getSessionStore();
const unsubscribe = store.subscribe(({ loaded, session }) => {
if (loaded) {
setUser(session?.user ?? null);
}
});
const { loaded, session } = store.getState();
if (loaded) {
setUser(session?.user ?? null);
}
return unsubscribe;// Core v10
return openmrsObservableFetch<FHIRAllergy>(url).pipe(map(({ data }) => mapAllergy(data)));
// Core v11
const { data } = await openmrsFetch<FHIRAllergy>(url);
return mapAllergy(data);The framework’s test mock of getCurrentUser now returns a Promise too, so tests that mocked it with rxjs of() need updating.
Workspace v1
Core v11 removes the workspace v1 API, including launchWorkspace, launchWorkspaceGroup, closeWorkspace, navigateAndLaunchWorkspace, useWorkspaces, WorkspaceContainer, ActionMenuButton and types such as DefaultWorkspaceProps. If a module’s routes.json still declares workspaces or workspaceGroups, the framework logs a warning and doesn’t register them.
Follow Migrate to Workspace v2 to move to workspaces2, workspaceWindows2 and workspaceGroups2.
Store state from useStore
In Core v10, useStore, useStoreWithActions and createUseStore returned a new object on every render, made by spreading the store’s state and any bound actions. In Core v11, unless you pass actions, they return the store’s state, or your selector’s result, itself.
- Don’t change the returned value in place. It’s the store’s own state, so changing it changes the store without notifying subscribers. Update the store with
setStateor with actions instead. - Effects and memoized values that depend on the result now only re-run when the state changes, not on every render.
- Selectors can return arrays and primitive values. Core v10 spread the result into an object, so an array came back as an object with numeric keys.
Pagination hooks
goTo,goToNextandgoToPreviousfromusePaginationanduseOpenmrsPaginationno longer change identity when the data or the current page changes. If you relied on them changing to reset the page, for exampleuseEffect(() => goTo(1), [goTo]), add the value that should trigger the reset, such as a search term, to the effect’s dependencies.useOpenmrsPaginationreturnscurrentPageSizeas a number instead of a ref. ReplacecurrentPageSize.currentwithcurrentPageSize.useOpenmrsPaginationreturns to page 1 by itself when its URL changes.goTofromusePaginationignores page numbers that aren’t positive integers, such as0or2.5, and logs a warning. Core v10 went to page 1 for numbers below 1, and used fractions as given. Numbers past the last page still go to the last page.
Expressions
Core v11 changes how the framework’s expression evaluator works. This affects display expressions, both displayExpression in routes.json and Display conditions in configuration, and React Form Engine expressions.
&&,||and??short-circuit, and optional chaining works as it does in JavaScript.- Reading a property of
nullthrows, as it does in JavaScript. Core v10 returnedundefined. For example, whenvisitisnull,!visit.stopDatetimeused to betrue, and now throws. Use optional chaining instead:!visit?.stopDatetime. - A missing property no longer falls back to a variable with the same name. In Core v10,
obs.valuereturned thevaluevariable whenobshad novalueproperty. - Expressions can’t use built-in object and function properties other than
toString,valueOfandhasOwnProperty, so calls such asfn.call(...)orMath.max.apply(...)now throw. - Computed property access with a variable key, such as
obj[key], now reads the property. Core v10 returned the value ofkeyitself. - Properties of strings, such as
name.length, now work.
When a display expression throws, O3 hides the extension and logs an error. Check the display expressions in your routes.json files and configuration for the patterns above.
navigate only accepts HTTP and HTTPS URLs
navigate() now throws if the target’s protocol isn’t http: or https:, for example mailto:, tel: or javascript:. Relative paths still work. ConfigurableLink calls navigate() when clicked, so the same applies to it. Use a plain <a href> for other kinds of links.
For distribution maintainers
Upgrade the app shell first
Modules built with Core v11 tooling (openmrs, @openmrs/webpack-config or @openmrs/rspack-config 11.0.0) load the Module Federation runtime from the app shell, and refuse to start under an older app shell. Modules built with older tooling still load under the Core v11 app shell, but they break if they use APIs that Core v11 removed, described above. Build your distribution with openmrs@11 before, or at the same time as, you upgrade to frontend modules built with Core v11 tooling. This was introduced in this pull request .
openmrs assemble fails on broken modules
openmrs assemble now fails if a frontend module is missing its routes.json, or the JavaScript entrypoint its import map entry points at. Pass --no-ensure-entrypoints to report these problems as warnings instead. This was introduced in this pull request .
Serve the precompressed assets
openmrs build now writes gzip (.gz) and brotli (.br) copies of each JavaScript, CSS, JSON, SVG and HTML file of 1 KB or more. Configure your web server to serve them; the Reference Application’s frontend nginx.conf shows how. To skip writing them, pass --no-compress to openmrs build, or set "compress": false in spa-build-config.json. See Create a distribution.
Remove offline settings
openmrs build and openmrs develop no longer accept --support-offline, and openmrs build ignores supportOffline in spa-build-config.json. The offline tools app is gone.
Check display expressions in your configuration
The expression changes also apply to Display conditions expressions in your distribution’s configuration. An expression that now throws hides its extension.
Update login location settings
The login app no longer has a chooseLocation.numberToShow setting; it was already ignored. If your configuration still sets it, remove it to clear the “Unknown config key” error. chooseLocation.locationsPerRequest now takes effect: it sets how many locations the location picker loads at a time.
Show your distribution’s version (optional)
openmrs assemble --application-version <version> records your distribution’s version in the routes registry. The navigation bar and login page show it.
Other changes worth knowing about
<Workspace2>no longer renders the workspace header itself. It reports the workspace’stitleandhasUnsavedChanges, and the framework renders the header around it. Keep wrapping workspace content in<Workspace2>.- A
workspaces2entry can set its ownwidth, which overrides its window’s width while that workspace is on top, and workspaces receive theirmetaasworkspaceMeta. Extensions can read their own metadata withuseExtensionContext. scopePatternfor workspace groups is matched against the path relative to the SPA base, such as/home/gastrodynamics. Patterns written against the full path still work.- The webpack and Rspack configs now honor a module’s own Browserslist configuration, in
package.jsonor.browserslistrc. Without one, they usebrowserslist-config-openmrs. useLocationsnow returns every page of locations, as REST objects in the default representation. Core v10 returned the first page, mapped touuidanddisplay.PageHeaderrenders its title as anh1.- The default
diagnosisTagscolors changed: primary diagnoses are now blue and secondary diagnoses gray, instead of red and blue. Colors your distribution configures still apply.DiagnosisTagsalso has a newshowCertaintyprop.
Migration examples
- Patient chart: peer dependency , offline removal , SWR
- Patient management: peer dependency , offline removal , SWR
- Form builder: peer dependency , SWR