RFC: Global context injection for extension display expressions (GSoC 2026 — Dynamic EHR)

The Problem

The extension displayExpression system currently evaluates JavaScript expressions to control extension visibility. However, the available expression context is limited to:

  • session — user, roles, privileges, sessionLocation

  • slotState — custom state passed via ExtensionSlot props

This limitation means that location tags, visit type, and encounter context cannot be accessed directly within expressions. Instead, they must be manually passed through every <ExtensionSlot state={{...}}> mounting point across the codebase, leading to significant prop-drilling.

Examples of currently unsupported expressions

"displayExpression": "visit.visitType.display === 'Inpatient'"
"displayExpression": "isLocationTagged('ICU')"
"displayExpression": "encounter.encounterType === 'ANC'"

This limitation represents a structural blocker for the GSoC 2026 project:

“Dynamic EHR: Custom screens based on user roles, locations, and other values.”


What I Have Already Built

To address related gaps, I have implemented the following groundwork:

Proposed Solution

I propose introducing a GlobalExpressionContextStore using a lightweight Zustand store that allows applications to write shared runtime context:

setExpressionContext('visit', currentVisit);
setExpressionContext('location', sessionLocation);
setExpressionContext('encounter', currentEncounter);

The evaluator in getAssignedExtensionsFromSlotData() would automatically merge this global context with:

  • existing session

  • existing slotState

This eliminates the need for prop-drilling.

Resulting capability

Implementers could then write expressions like:

"displayExpression": "visit.visitType.display === 'Inpatient'"
"displayExpression": "isLocationTagged('ICU')"
"displayExpression": "hasRole('Nurse') && visit != null"

Questions for Mentors

  1. Architecture

    Is a global context store the right pattern, or is there a preferred approach for sharing runtime context across the extension system?

  2. Placement

    Should this implementation live in:

    • esm-extensions, or

    • esm-react-utils?

  3. Security Considerations

    Are there any security or sandboxing concerns with exposing visit/encounter data to arbitrary expressions?

  4. Existing Patterns

    Is there an existing global state pattern in the codebase that applications can write to and that I should align with?

cc: @jayasanka , @dkayiwa , @dennis , @ibacher , @bawanthathilan

Reading through this and the open work on esm-core#1682, the role/privilege side of the context is taking shape. One area I haven’t seen pinned down is the location and visit half of the RFC , Sourav raised the same gap on thread/48930 (location, current visit, encounter).

A few questions on how you see that landing:

  1. Should sessionLocation (uuid + display) live in the same expression context as hasPrivilege, or be exposed as a separate userLocation helper to keep the surface area predictable?

  2. For currentVisit / currentEncounter, those are patient-scoped — does the evaluator context get them only inside the patient chart slots, or do we need a cross-app state that also handles “no patient selected” gracefully?

  3. On reactivity, hasPrivilege re-evaluates on session updates. Visit and encounter change much more often. Is debounce / equality-check on the slotState the right place to handle that, or do we need a separate invalidation path?

I’m planning to scope a small PR around (1) since it’s the cheapest slice and unblocks the location-conditional extensions case without touching what you have in flight on #1682. If that overlaps with what you’re already drafting let me know before I open it.

Question 3: Visits/encounters are expected to change much faster than the session state, so if we rely on the store subscription update to trigger the evaluation, it might prove costly quite quickly. For #1702, I faced a problem where useLayoutType was being invoked on every resize event; instead, if the debounce was applied to the store subscription function (not to the slot), it would have allowed for a constant evaluation rate without sacrificing reactivity. It would depend on whether the time constraint required by the visibility change is that strict.

Hey @praneeth622, I’m already drafting the location context slice as part of the broader GlobalExpressionContextStore work. Let’s sync before you open anything to avoid overlap. Happy to pair on it.

Good questions. On (1) I lean toward keeping sessionLocation flat in the same context object alongside hasPrivilege for simplicity, but open to a userLocation helper if the surface area concern is strong. On (2) global store with a null-safe default for the no-patient case feels right. On (3) agreed with @ashthe25, debounce on the store subscription rather than slot level is cleaner.

Your explanation about (2) is really helpful. Indeed, having a safe default in the store seems to be okay. For example, getExpressionContext(‘visit’) ?? null would be great for keeping slot-level logic untouched while not interfering with those expression evaluations which do not require visit context information. On (3), having debouncing in sync with equality comparison from #1687 should solve double firing issues.

1 Like

Thanks Soham. Quick context: openmrs-esm-core#1756 is already closed @ibacher pointed out on O3-5623 that the full session object (including sessionLocation.uuid and .display) is already spread into the expression context at extensions.ts line 353, so a displayExpression can access it as session.sessionLocation.uuid today.

So the “location context slice” isn’t a missing capability, it’s a surface-area/ergonomics question. If your GlobalExpressionContextStore RFC is broader than that, new reactive sources, patientUuid scope, cross-slot dedup I’d be interested to read it once it’s up. Happy to review.

Not pairing this ticket since the underlying variable is already there; marking O3-5623 as Won’t Do.

esm-react-utils is primarily a thin layer over the framework and a couple of React utilities. Almost nothing about the extension system should be primarily implemented there, just enough of a layer to make it work with React.

Good catch on line 353, that clears up the sessionLocation gap cleanly. The RFC is really targeting what’s still missing: currentVisit, currentEncounter, and patientUuid patient-scoped reactive sources that aren’t in the context today and can’t be passed without prop-drilling through every ExtensionSlot. That’s the core of the GlobalExpressionContextStore proposal.

Got it store lives in esm-extensions, with just a thin React hook in esm-react-utils to expose it. Thanks for clarifying!