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.

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 consistent 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 useSWR 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

  • Do not manually edit any of the locale-speficic translation files in the translations directory. Run the extract-translations script instead to keep the en.json file in sync with the translation keys in the codebase.

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

Event handlers

  • Ensure that your event handlers properly define their cleanup logic. Event handlers in useEffect callbacks should always return a cleanup function to prevent unexpected behaviour. For example, do this:

    useEffect(() => {
      const handleClick = () => {
        // ... handle the click event
      };
     
      document.addEventListener("click", handleClick);
     
      return () => {
        document.removeEventListener("click", handleClick);
      };
    }, []);

    Instead of this:

    useEffect(() => {
      const handleClick = () => {
        // ... handle the click event
      };
     
      document.addEventListener("click", handleClick);
     
      // Incorrect cleanup: Executes immediately, not when the component unmounts
      document.removeEventListener("click", handleClick);
    }, []);

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.

  • Use the jest.mocked utility to preserve type information when mocking functions in tests. For example:

    Prefer:

    const mockedShowSnackbar = jest.mocked(showSnackbar); // All the type information is preserved

    Over:

    const mockedShowSnackbar = showSnackbar as jest.Mock;

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.

  • Use the classnames (opens in a new tab) library to conditionally apply styles to an element. Consider using classnames if you're interpolating multiple class names into a string. For example, the following snippet:

    <NumberInput
      allowEmpty
      className={`${styles.textInput} ${val.className}`}
      // other props omitted for brevity
    />

    Could be replaced by:

    import classnames from "classnames";
     
    <NumberInput
      allowEmpty
      className={classNames(styles.textInput, val.className)}
      // ... other props omitted for brevity
    />;

    The following snippet shows a more advanced case - a div styled with multiple conditional styles:

    return (
      <div
        className={`${styles.textInputContainer} ${disabled && styles.disabledInput} ${
          !isWithinNormalRange && styles.danger
        } ${useMuacColors ? muacColorCode : undefined}`}
      >
        // ... details omitted for brevity
      </div>
    );

    You can refactor this snippet to leverage classnames as follows:

    import classNames from "classnames";
     
    const containerClasses = classNames(styles.textInputContainer, {
      [styles.disabledInput]: disabled,
      [styles.danger]: !isWithinNormalRange,
      [muacColorCode]: useMuacColors,
    });
     
    return <div className={containerClasses}>// ... details omitted for brevity</div>;

Search inputs

  • Debounce search inputs to prevent unnecessary requests to the backend. Use the useDebounce (opens in a new tab) hook to debounce search inputs. Here's a snippet (some bits are omitted for brevity) showing how you could use the hook:

    const [searchTerm, setSearchTerm] = useState("");
    const debouncedSearchTerm = useDebounce(searchTerm);
     
    return (
      <TableToolbarSearch
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
        placeholder={t("searchThisList", "Search this list")}
      />
    );
     
    // Do something with the debouncedSearchTerm
  • Use fuzzy (opens in a new tab) to implement fuzzy search. Fuzzy search is a strategy for matching search terms that are similar to, but not exactly the same as, the search term. For example, if the search term is John, fuzzy search will match Jon, Jhon, and Johhn. This is useful for matching search terms that are misspelled or contain typos. Here's how we can leverage fuzzy to enhance the search experience from the snippet above:

    const [filter, setFilter] = useState("");
     
    const filteredForms: Array<TypedForm> = useMemo(() => {
      if (!debouncedSearchTerm) {
        if (filter === "Retired") {
          return forms.filter((form) => form.retired);
        }
     
        if (filter === "Published") {
          return forms.filter((form) => form.published);
        }
     
        if (filter === "Unpublished") {
          return forms.filter((form) => !form.published);
        }
     
        return forms;
      }
     
      return debouncedSearchTerm
        ? fuzzy
            .filter(debouncedSearchTerm, forms, {
              extract: (form: TypedForm) => `${form.name} ${form.version}`,
            })
            .sort((r1, r2) => r1.score - r2.score)
            .map((result) => result.original)
        : forms;
    }, [filter, forms, debouncedSearchTerm]);

    We're using the debouncedSearchTerm from the snippet above to filter the list of forms. We're also using the extract option to tell fuzzy how to extract the search term from the form. In this case, we're extracting the search term from the form's name and version. This is because we want to match forms that contain the search term in their name or version. Finally, we're sorting the results by score, which is a measure of how closely the search term matches the form.

Testing