Code Organization
Colocate related files in the same directory. Each component should have its own directory containing all its associated files. This includes:
Component file (e.g.,
patient-banner.component.tsx) - This is a React component.Test file (e.g.,
patient-banner.test.tsx) - This is a Jest test file.Stylesheet (e.g.,
patient-banner.scss) - This is a CSS stylesheet.Resource file (e.g.,
patient-banner.resource.ts) - This is a file that contains data fetching logic.Any other related files (e.g., constants, types, utilities)
Example directory structure:
src/
└── patient-banner/
├── patient-banner.component.tsx
├── patient-banner.test.tsx
├── patient-banner.scss
└── patient-banner.resource.tsThis approach:
Makes it easier to find and modify related files
Simplifies refactoring and maintenance
Keeps the codebase modular and well-organized
Makes it clear which files belong to which component
Avoid placing styles for multiple components in the same stylesheet. Instead, create a separate stylesheet for each component. This makes it easier to find the styles for a particular component.
Use the template app
to quickly seed new O3 frontend modules:
Visit the template repository
Click the green
Use this templatebuttonChoose
Create a new repositoryFollow the setup instructions in the template's README
The template provides:
Correct TypeScript configuration
Pre-configured testing setup with Jest
ESLint and Prettier configurations
GitHub Actions workflows
Basic project structure following O3 conventions
Example components and tests
Group imports alphabetically based on their type. The recommended order is:
React and framework imports (e.g.,
React,useState,useEffect)External modules (e.g.,
lodash,dayjs,react-i8next)Carbon component imports (e.g.,
Button,InlineLoading)OpenMRS imports (e.g.,
@openmrs/esm-framework,@openmrs/esm-patient-common-lib)Local imports (components, hooks, utilities, etc.)
Asset imports (e.g
import styles from './user.scss')
In the near future, we'll be able to use ESLint import order sorting to enforce this convention. Following this convention makes it easier to maintain consistency across the codebase.
Consolidate library imports into a single import statement. This makes it easier to see which modules are being used and makes the code more readable. For example, prefer:
// Good import { Button, InlineLoading } from '@carbon/react';
Over:
// Bad
import { Button } from '@carbon/react';
import { InlineLoading } from '@carbon/react';Note that you should still keep imports from different modules separate:
// Good - separate imports for different modules
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, DataTable } from '@carbon/react';
import { useConfig, showSnackbar } from '@openmrs/esm-framework';Type imports should be marked with the type keyword. An ESLint plugin will automatically flag any type imports that are not marked with the type keyword.
// Good
import { showModal, type Visit } from '@openmrs/esm-framework';Place type annotations and interfaces at the top of the file, after the imports and above any component code. Since TypeScript types and interfaces are development-time constructs that get removed during compilation, they don't affect the runtime behavior of your code.
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@carbon/react';
import { showSnackbar } from '@openmrs/esm-framework';
// Type definitions come after imports, before component code
interface UserData {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
type UserComponentProps = {
userData: UserData;
onSave: (data: UserData) => void;
isEditable?: boolean;
};
// Component code follows type definitions
export const UserComponent: React.FC<UserComponentProps> = ({
userData,
onSave,
isEditable = false
}) => {
// ... component implementation
};Some key points about type placement:
Keep related types and interfaces grouped together
Place more generic types before more specific ones that might depend on them
Consider extracting commonly used types into a separate
types.tsfile if they're used across multiple components
On whether to use type aliases or interface declarations:
Use
interfacewhen you need declaration merging or inheritance.Use
typefor unions, intersections, primitives, tuples, and utility types.Be consistent within your codebase - if your team has standardized on one approach, follow that convention.
// Use interface for object shapes
interface UserProps {
name: string;
age: number;
onSave: (data: User) => void;
}// Use type for unions and more complex types
type Status = 'loading' | 'success' | 'error';
type ButtonKind = 'primary' | 'secondary' | 'ghost';
type Nullable<T> = T | null;