Skip to Content
DocsCoding conventionsState management

State management

We use various approaches to manage state in O3, from React’s built-in hooks to framework-provided Zustand stores. Read more about state management in this recipe. Below are some general guidelines for state management:

  • Follow the guidelines outlined here .

  • Keep state as close as possible to the component that needs it.

  • To share state between components, lift the state up to the nearest common ancestor  of the components that need to share the state and pass the state down to the components as props. This is the simplest way to share state between components.

  • Avoid creating state variables for things that can be computed from existing state variables . For example, if you have a state variable called firstName and another called lastName, don’t create a third state variable called fullName. Instead, derive the fullName from the firstName and lastName state variables.

  • Follow state immutability principles:

    • Never modify state directly.
    • Use spread operators or immutable update patterns.
    • Use state updater functions for state that depends on previous state.
  • Consider using React’s built-in hooks to optimize performance:

    • Use useMemo for expensive computations.
    • Use useCallback for memoized callbacks.
    • Split state into smaller pieces to prevent unnecessary re-renders.
  • Don’t use global state for data that should be scoped to a specific component or a small set of components.

  • When state must be shared across microfrontends or across unrelated React trees, use the framework store APIs from @openmrs/esm-framework rather than importing Zustand directly. Define the store once at module scope with a unique name, type its shape, and select only the slice a component needs:

    import { createGlobalStore, useStore } from '@openmrs/esm-framework'; interface VisitFilterStore { filters: { selectedStatus: string | null; }; } export const visitFilterStore = createGlobalStore<VisitFilterStore>('visit-filter', { filters: { selectedStatus: null, }, }); function VisitFilterSummary() { const { selectedStatus } = useStore(visitFilterStore, (state) => state.filters); return <span>{selectedStatus ?? t('allStatuses', 'All statuses')}</span>; }
  • Put store updates behind small actions when multiple components need to update the same store. useStore can bind action functions so callers do not need to repeat setState details:

    const visitFilterActions = { setSelectedStatus: (state: VisitFilterStore, selectedStatus: string | null) => ({ filters: { ...state.filters, selectedStatus, }, }), }; const { selectedStatus, setSelectedStatus } = useStore( visitFilterStore, (state) => state.filters, visitFilterActions, );
  • Use createGlobalStore(name, initialState, 'sessionStorage') only for values that should survive a page refresh in the current browser tab. Persistent clinical data should live in the backend, and secrets or patient-identifying data should not be placed in browser storage unless the feature explicitly requires it and the privacy tradeoff has been reviewed.

  • Use App Context for state that should exist only while a feature or app area is active. Define the namespace with useDefineAppContext, read it with useAppContext, and handle undefined explicitly because it means the namespace is not currently registered:

    interface DateFilterContext { dateRange: Date[] | null; setDateRange: (dateRange: Date[] | null) => void; } useDefineAppContext<DateFilterContext>('laboratory-date-filter', { dateRange, setDateRange, }); const dateRange = useAppContext<DateFilterContext, Date[] | null>( 'laboratory-date-filter', (context) => context?.dateRange ?? null, );
Last updated on