EDS4 logo

Coding style guide

A set of established conventions about how we write code for EDS.

Developing in a design system or component library is a little bit different than developing an application. In product development, the focus is largely on the end-user experience, performance, and the deadline to ship it.

In design system development, we focus primarily on the consumers and maintainers of our design system (designer/developer/product manager e.g.) but also on the end-user.

We keep a habit of looking for potential patterns, researching and promoting best practices, and making way for a top-of-the-line DX (developer experience) – whilst at the same time always keeping UX and accessibility in mind.

To ensure we can continue to maintain EDS long into the future, having a consistent coding style is important to keep the code base readable and maintainable. The guidelines below have been adopted for the EDS repo.

  • We use Arrow function expressions, sometimes called "fat arrows", when defining React components.
  • When exporting the components, we prefer named exports over default exports, because they are easier to trace for new maintainers.
Do
export const NewComponent = () => {
return;
};
Don't
function NewComponent() {
return;
}
export default NewComponent;

All our code is written in TypeScript. TS is great in helping us maintain our design system with all of its components, props and logic. Although there is the rare case of @ts-ignore in the codebase, this should be avoided at all costs.

  • We prefer to create our type definitions using type aliases, rather than interface. Learn more about the difference.
  • We always keep required props at the top, and optional at the bottom of the type object.
  • If it's a type alias for an exported component, always name the alias after the component name but with a suffix of Props.
type MyComponentProps = {
propOne: boolean;
propTwo?: boolean;
};
  • Always start out keeping the type definitions inside the component file that uses them, at the very top.
  • If you find yourself splitting up your component into multiple files that share the same types then it's a good idea to move all the types into a separate file, name it types.ts, and keep it in the same folder as the component file.

When extending types via intersections we prefer to order them in a way that's going to make sense on the {package-name}/props page.

For example, you might think name is the most important prop of the <Bear /> component so you place AnimalProps before the new type definitions:

type AnimalProps = {
name: string;
};
type BearProps = AnimalProps & {
honey: boolean;
};
  • Always use camelCase.
  • Always try to keep prop names as concise as possible, but if it's not considered intuitive enough don't be afraid to have a long prop name.
  • Avoid is or has prefixes on boolean props, e.g. do disabled rather than isDisabled. However it's fine to use these prefixes on inner component logic, like when naming constants for example.
  • If it's a boolean prop, prefer to name it after what default value it has, e.g. if hidden is a prop that's false by default, it will make sense when implemented like <Component hidden />, but if it was true (hidden) by default it would make more sense to name it visible.
  • If the prop is for a single CSS property or HTML attribute, then name it exactly after that, e.g. maxHeight rather than tallest, and onClick rather than action.
  • When the prop informs multiple styles or features it is fine to come up with a prop name that fits, e.g. size that could inform both height, width and padding. Be careful not to introduce new naming conventions, look for inspiration in our existing components first.
  • Always include a /** TSDoc comment */ that describes what the prop does, and what the default value is if relevant.

We prefer to abstract third-party library props to our own, unless they are already consistent with our existing naming convention of course.

NOTE: We learnt this the hard way with the SelectInput that uses react-select, making it hard to switch to another select-library – should we wish to – as the prop names will likely not match any more and we'll be stuck with react-select sounding prop names.

We prefer to define any default prop values in the arrow function's parameters, at the top of the component, before the return.

Do
type WidthComponentProps = {
/** Contrived example. */
width: string;
};
const WidthComponent = ({ width = '10rem' }: WidthComponentProps) => {
return width;
};
Don't
type WidthComponentProps = {
/** Contrived example. */
width: string;
};
const WidthComponent = ({ width }: WidthComponentProps) => {
return width ?? '10rem';
};

We like destructuring when it helps making the code easier to read; most commonly when a variable is used multiple times.

Do
const { value } = event.target;
const isValid = validateInput(value);
return isValid && value ? value : '';
Don't
const isValid = validateInput(event.target.value);
return isValid && event.target.value ? event.target.value : '';
  • As a general rule, we prefer code that is easy to read and understand, over the most optimised or smart code.
  • We prefer React Fragments in the shorter way: <></>.
  • We prefer "early single line returns", i.e. if (!expression) return; doSomething();, over if (expression) { doSomething(); } else { return; }.
  • We have a great set of ESLint rules in the repo, that help you stay in line with our conventions. Check out the .eslintrc.js file for a complete list.
  • We recommend having the "Format on Save" option (or equivalent) set up in your IDE so you save yourself a lint error in the pipeline on Bitbucket.
  • We prefer to declare our useState()s and useRef()s at the very top of the component.