Docs
Coding conventions

Coding conventions

This is a compendium of the coding conventions we use in O3. The purpose of this document is to help us write code that is consistent and easy to maintain.

General

Naming

  • Follow the guidelines in this naming cheatsheet (opens in a new tab).
  • Use camelCase for variables, functions, methods, and class names.
  • Use kebab-case for file names and folder names.
  • Components should contain the .component suffix in their name (e.g. user.component.tsx). This nomenclature is used to distinguish components from other files such as resources, stylesheets, and tests, and determines where translation keys and strings should be extracted from. Translation keys and strings will not be extracted from files that do not match this convention.
  • Unit and integration test files should contain the .test suffix in their name (e.g. user.test.tsx). Do not include the word component in the test file name.
  • Playwright e2e tests should contain the '.spec' suffix in their name (e.g. user.spec.ts).
  • Stylesheets should not contain .component suffix in their name (e.g. user.component.scss). This is because stylesheets are not components, and are not translated. Instead, stylesheets should be named after the component they are styling (e.g. user.scss).
  • Resource files that encapsulate data fetching logic should contain the .resource suffix in their name (e.g. user.resource.ts). This is to distinguish them from other files such as components, stylesheets, and tests.
  • Name TypeScript files that contain JSX with the .tsx extension (e.g. user.component.tsx). Name TypeScript files that do not contain JSX with the .ts extension (e.g. user.resource.ts). In most cases, you shouldn't need to use the .tsx extension for files outside the src directory.
  • Follow the extension system nomenclature guide when naming your extensions and extension slots.
  • Use the file name as the component name. For example, user.component.tsx should contain a component named UserComponent. This makes it easier to find the component in the codebase.
  • Avoid using DOM component prop names for different purposes. For example, avoid using the className prop to pass a CSS class name to a component. Instead, use a prop name that is specific to the component, such as cssClass.
  • Use camelCase for prop names. This is consistent with the naming convention for variables, functions, and methods.
  • Translation keys should be in camelCase whereas translation strings should be in sentence case. For example, firstName is a translation key whereas First name is it's corresponding translation string.
  • Frontend modules in monorepos should have names that start with the esm- prefix. The name of the module should describe what the module does. For example, esm-user-management is a good name for a frontend module handling user management concerns.
  • Event handler props should be named after they event they handle e.g. onClick for a click handler. By convention, event handler props should start with the on prefix, followed by a capital letter.
  • State updater functions should be named after the state they update. For example, setFirstName is a good name for a state updater function that updates the firstName state.
  • What to name your branches is typically down to personal preference. However, when in doubt, name your branches using the conventional commit (opens in a new tab) type that your work conforms to, followed by a slash and a short dash-separated description of the work. Good examples include: feat/debounced-order-basket-search, fix/missing-translation, chore/bump-dependencies and refactor/remove-unused-code.

Project structure

  • Monorepos should contain domain-specific packages that are related to each other. For example, patient management concerns such as registration and search live in the openmrs-esm-patient-management monorepo.
  • Configuration files should generally exist at the top level of the monorepo directory. Notable exceptions to this rule include the a file containing helpers for tests, the i18next-parser configuration, and setupTests.ts, which should all exist in the tools directory.
  • Colocate files that are related to each other. For example, a component and its corresponding test and stylesheet should live in the same directory. This way, when you make a change to a component, it's easy to extend that change to the test and stylesheet if necessary.
  • 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.

Components

  • Don't keep unused code in your components. Keeping dead code around can cause confusion and makes it harder to maintain the codebase.
  • Validate the props passed to your component using type aliases or interfaces. This helps to catch bugs early and makes it easier to understand how the component is used.
  • Make sure you read through the Component API for a particular Carbon component before using it. This helps you to understand the component's props and how to use them. It also helps you to understand the component's behavior and can obviate the need for writing custom code. For example, here's the Component API for the Button component (opens in a new tab).
  • Use keys in lists (opens in a new tab). This helps React to identify which items have changed, been added, or been removed. This is especially important if you are rendering a list of components that contain state.
  • Generate keys from the data itself (opens in a new tab) if possible. For example, if you are rendering a list of patients from the database, use the patient's ID as the key. This ensures that the key is unique and stable across renders.
  • Avoid using effects (opens in a new tab) for things that don't involve synchronizing with external systems. The distinction is nuanced and can be difficult to understand. Please read and internalize the linked article before reaching for effects.
  • Don't reach for performance optimizations like memo, useMemo and useCallback before you need them. These optimizations come at a cost, and can make your code harder to understand. Read this article (opens in a new tab) and this article (opens in a new tab) to understand when to use these hooks.
  • Omit the value of a prop when it is explicitly true. For example, <UserComponent isAdmin /> is preferred over <UserComponent isAdmin={true} />.
  • Follow consisent code formatting, naming conventions and folder structure. This makes the codebase more readable and easier to maintain.

Data fetching

  • Colocate your data fetching logic in a file suffixed with .resource. For example, user.resource.ts contains the data fetching logic for the user resource.
  • Wherever possible, prefer abstracting your data fetching into a custom hook rather than fetching with effects (opens in a new tab). Fetching data with effects has many downsides (opens in a new tab) and should be avoided. Instead, prefer using SWR (opens in a new tab) hooks.
  • Use SWR (opens in a new tab) hooks to fetch data from the backend. Use SWRImmutable for resources that are not expected to change often, such as backend configurations.
  • Put the SWR hook in a separate file, and export it as a function. This allows us to reuse the same hook in multiple components.
  • Memoize the return value of your SWR hook using useMemo to prevent unnecessary rerenders. This is especially important if the hook is used in a component that is rendered many times, such as a table row.
  • Data fetching hooks should follow the naming convention use<resource>. For example, useUser is the hook for fetching user data.
  • Use openmrsFetch to fetch data from the backend. openmrsFetch is a wrapper around the fetch API that adds authentication and authorization headers and handles errors. Pass it to as the fetcher argument of your SWR hooks.
  • Use openmrsObservableFetch only if you need to fetch data from the backend using an observable. This is useful for streaming data from the backend. Ensure you understand the difference between observables and promises before reaching for this function.

Internationalization

  • Use the useTranslation (opens in a new tab) hook to translate strings in your components.

  • Use the Trans (opens in a new tab) component to translate strings that contain HTML tags.

  • To handle pluralization, use the following pattern:

    // If there's only one risk flag, the string "1 risk flag" is displayed.
    // If there are multiple risk flags, the string "{count} risk flags" is displayed
    // e.g. "3 risk flags".
    <span className={styles.flagText}>
      {t("flagCount", {
        count: riskFlags.length,
      })}
    </span>

    The corresponding keys and strings for the code above should look like this:

    "flagCount_one": "{{ count }} risk flag",
    "flagCount_other": "{{ count }} risk flags"

State management

Mutations and side effects

  • Use SWR's global and bound mutate (opens in a new tab) APIs to mutate data in the cache. This ensures that the cache is updated consistently across the application and omits the need to reload the page to see the changes.
  • Show a toast notification when a mutation succeeds. When a mutation fails, show a inline notification with an error message that communicates the reason for the failure.

Type annotations

  • Follow the guidelines outlined in React TypeScript Cheatsheets (opens in a new tab).

  • Always annotate your function parameters with types. This makes it easier to understand what the function does, and explicitly expresses the function's contracts.

  • Rely on TypeScript's type inference for things like variable and array initialization, and in some cases, function return types. The goal of the type system is not to annotate every single variable with a type, but rather to make sure that the important parts of your code are type-safe. Read more about type inference here (opens in a new tab).

  • TypeScript interfaces enable declaration merging and can be extended by other interfaces. This makes them more flexible than type aliases, which cannot be extended. If you don't need these features, prefer using type aliases instead.

  • Don't use any unless you absolutely have to. Instead, use unknown or never to express the fact that you don't know the type of a variable or that a function never returns.

  • Wherever possible, use the import type syntax when importing types. This prevents the type from being imported at runtime, which reduces the bundle size. For example:

    // Prefer
    import type { User } from "@openmrs/esm-user-management";
     
    // Instead of
    import { User } from "@openmrs/esm-user-management";
  • Prefer union types over status enums (opens in a new tab). For example, prefer type Status = "loading" | "error" | "success" over enum Status { Loading, Error, Success }. This is because enums are not type safe, and can be assigned any value. For example, Status.Loading = "error" is a valid statement, but Status = "error" is not.

Styling

  • Be wary of using global styles. They can easily lead to unintended side effects and make it difficult to reason about the codebase. Nest global styles under a class name to prevent them from affecting other components.

    // Avoid applying styles globally
    :global(.cds--text-input) {
      height: 3rem;
      @extend .label01;
    }
     
    // Prefer scoping style overrides under a class name
    .input-group {
      display: flex;
      justify-content: center;
      flex-direction: column;
     
      :global(.cds--text-input) {
        height: 3rem;
        @extend .label01;
      }
    }
  • Put Carbon style overrides in overrides.scss (opens in a new tab). This ensures that the overrides are applied consistently across the application.

  • Prefer using Carbon color (opens in a new tab), spacing (opens in a new tab) and type (opens in a new tab) tokens over hard-coded values. Below are some examples of using tokens in code:

    @use "@carbon/styles/scss/colors";
    @use "@carbon/styles/scss/spacing";
    @use "@carbon/styles/scss/type";
     
    .listWrapper {
      margin: spacing.$spacing-05;
    }
     
    .resultsCount {
      @include type.type-style("label-01");
    }
     
    .sortDropdown {
      color: colors.$gray-100;
      gap: 0;
    }

    Find a useful reference for color token mappings here (opens in a new tab).

  • Use SASS features (opens in a new tab) like interpolation, at-rules, mixins, and functions to make your styles more reusable and maintainable.

  • If you want to apply styles based on the user's viewport size, use our predefined breakpoints (opens in a new tab). For example, to apply different styles for tablet and desktop viewports, do this:

    // Tablet viewports
    :global(.omrs-breakpoint-lt-desktop) {
      .form {
        height: calc(100vh - 9rem);
      }
    }
     
    // Desktop viewports
    :global(.omrs-breakpoint-gt-tablet) {
      .form {
        height: calc(100vh - 6rem);
      }
    }
    ℹ️

    Make sure to scope your styles under a class name (such as .form in the example above) to avoid them affecting other components.

Testing