Code Organization

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

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

  1. Visit the template repository

  • 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.

  • 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.ts file if they're used across multiple components

On whether to use type aliases or interface 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;