Skip to Content
DocsMigration guidesMigrate to Core v11

Migrating to Core v11

This guide is for migrating frontend modules to Core v11 and above. v11 has several breaking changes including:

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@next

If 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:

webpack.config.js
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:

rspack.config.js
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.1

The 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, subscribePrecacheStaticDependencies and messageOmrsServiceWorker, usually found in startupApp or an offline.ts file. 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 returns true.
  • Remove the rest of the offline API. Functions such as queueSynchronizationItem, getSynchronizationItem, syncOfflinePatientData, getOfflinePatientDataStore, generateOfflineUuid, isOfflineUuid, isOnline, getCurrentOfflineMode and subscribeConnectivity are no longer exported. fetchCurrentPatient no longer takes an includeOfflinePatients argument.
  • Remove online and offline from routes.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:

FunctionCore v10Core v11
getCurrentUser()Observable that emits each time the session changesPromise that resolves once
getLocations()ObservablePromise<Array<Location>>
getVisitTypes()ObservablePromise<Array<VisitType>>
openmrsObservableFetch()ObservableRemoved. 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 setState or 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, goToNext and goToPrevious from usePagination and useOpenmrsPagination no longer change identity when the data or the current page changes. If you relied on them changing to reset the page, for example useEffect(() => goTo(1), [goTo]), add the value that should trigger the reset, such as a search term, to the effect’s dependencies.
  • useOpenmrsPagination returns currentPageSize as a number instead of a ref. Replace currentPageSize.current with currentPageSize.
  • useOpenmrsPagination returns to page 1 by itself when its URL changes.
  • goTo from usePagination ignores page numbers that aren’t positive integers, such as 0 or 2.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 null throws, as it does in JavaScript. Core v10 returned undefined. For example, when visit is null, !visit.stopDatetime used to be true, 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.value returned the value variable when obs had no value property.
  • Expressions can’t use built-in object and function properties other than toString, valueOf and hasOwnProperty, so calls such as fn.call(...) or Math.max.apply(...) now throw.
  • Computed property access with a variable key, such as obj[key], now reads the property. Core v10 returned the value of key itself.
  • 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() 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’s title and hasUnsavedChanges, and the framework renders the header around it. Keep wrapping workspace content in <Workspace2>.
  • A workspaces2 entry can set its own width, which overrides its window’s width while that workspace is on top, and workspaces receive their meta as workspaceMeta. Extensions can read their own metadata with useExtensionContext.
  • scopePattern for 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.json or .browserslistrc. Without one, they use browserslist-config-openmrs.
  • useLocations now returns every page of locations, as REST objects in the default representation. Core v10 returned the first page, mapped to uuid and display.
  • PageHeader renders its title as an h1.
  • The default diagnosisTags colors changed: primary diagnoses are now blue and secondary diagnoses gray, instead of red and blue. Colors your distribution configures still apply. DiagnosisTags also has a new showCertainty prop.

Migration examples

Last updated on