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.ts
This 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
- Component file (e.g.,
-
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 (opens in a new tab) to quickly seed new O3 frontend modules:
- Visit the template repository (opens in a new tab)
- Click the green
Use this template
button - Choose
Create a new repository
- Follow 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.
- React and framework imports (e.g.,
-
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 thetype
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.ts
file if they're used across multiple components
On whether to use
type
aliases orinterface
declarations:- Use
interface
when you need declaration merging or inheritance. - Use
type
for 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;