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 wordcomponent
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 thesrc
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 namedUserComponent
. 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 ascssClass
. - 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 insentence case
. For example,firstName
is a translation key whereasFirst 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 theon
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 thefirstName
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
andrefactor/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, andsetupTests.ts
, which should all exist in thetools
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 thefetch
API that adds authentication and authorization headers and handles errors. Pass it to as thefetcher
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
- Follow the guidelines outlined here (opens in a new tab).
- To share state between components, lift the state up to the nearest common ancestor (opens in a new tab) of the components that need to share the state and pass the state down to the components as props. This is the simplest way to share state between components.
- Avoid creating state variables for things that can be computed from existing state variables (opens in a new tab). For example, if you have a state variable called
firstName
and another calledlastName
, don't create a third state variable calledfullName
. Instead, derive thefullName
from thefirstName
andlastName
state variables.
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 thantype
aliases, which cannot be extended. If you don't need these features, prefer usingtype
aliases instead. -
Don't use
any
unless you absolutely have to. Instead, useunknown
ornever
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"
overenum 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, butStatus = "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
-
Avoid testing implementation details. Instead, test the component's public API. This makes it easier to refactor the component without having to rewrite the tests.
-
Follow the guidelines outlined here (opens in a new tab).
-
Don't make these common testing mistakes (opens in a new tab).
-
Structure large test suites using object page models (opens in a new tab) when writing e2e tests using Playwright.
-
Follow the e2e testing best practices (opens in a new tab) outlined in the Playwright docs.
-
Functions from
@openmrs/esm-framework
get mocked (opens in a new tab) automatically when running tests. To override the default mock, use thejest.mock
API. For example:// Override the default mock for usePatient jest.mock("@openmrs/esm-framework", () => ({ ...jest.requireActual("@openmrs/esm-framework"), usePatient: jest.fn(() => ({ uuid: "some-uuid" })), }));
Remember to update the mock when you modify or add a new function to
@openmrs/esm-framework
.