JS code standards

This document outlines the rules for writing JavaScript across our codebase.

General Principles

Progressive Enhancement

JavaScript should progressively enhance a page.

On all publicly accessible websites, core content and functionality should still be available without JavaScript. JavaScript applications written for a more specific audience which won’t be indexed by search engines may be more lax, but should still treat progressive enhancement as a guiding principle.

Performance

  • Cache DOM queries — only select an element once.

  • Use event delegation as much as possible to reduce the number of events bound to the page

  • Understand when you cause an element repaint and reduce inline style manipulations accordingly.

  • If possible, always use CSS for element transforms and transitions.

Element hooks

We should target page element in JavaScript using the data-js element attribute instead of using the class attribute. Classnames should be used for styling only.

e.g. <a data-js="index-link" href="/index">Index</a>.

TypeScript

TypeScript vs JavaScript

For any medium-large JavaScript applications, consider using TypeScript in preference to JavaScript, as it offers a better developer experience, and increased safety. See Transitioning to TypeScript for more details.

Remember that TypeScript gives you the ability to progressively type your project; it isn’t an all or nothing commitment. If you like, you can convert JavaScript files to TypeScript one at a time, if migrating an existing project.

TypeScript is currently used in the following projects:

Patterns

Importing types

You can use the type keyword when importing a type, e.g.:

import type { ReactNode } from "react";

This makes it clear that a type has been imported, rather than a component or class, and also provides some minor build optimisation via type erasure.

How do I type that?

NPM now displays a link beside the project name in the header with a direct link to the “Definitely Typed” types for the project. Sometimes however, you’ll still have difficulty finding the appropriate type. A handy escape hatch you can make use of when starting out, is to declare a placeholder type called TSFixMe, aliased to any. Using this type means that the object in question is effectively untyped, however it is easily grepable for later improvement.


export type TSFixMe = any;

usage:


type Props = {
  name: string;
  confusingThing: TSFixMe; // to be typed later
};

Similarly ts-ignore can be used as a last ditch escape hatch, but it’s use is strongly discouraged:

if (false) {
  // @ts-ignore: Unreachable code error
  console.log("boom");
}

Typing react components

Props

Typically you’ll want to provide a type for props and a return type.

props should be typed in the same file as the component, and typically a functional component will have the return type JSX.Element. TypeScript will infer a react component’s type as JSX.Element so you do not need to explicitly include it.

type Props = {
  name: string;
  age: number;
}

const Person = ({ name, age }: Props): JSX.Element => ()

Add types for any functions defined in the same module as you see fit.

Destructured props with ...rest

A common pattern in react is to spread ...rest into a child component, allowing you to pass attributes to children without manually defining the props in the parent component.

Typically, these props will be spread into either an html element, or another react component. The following are some helpful patterns for managing this with TypeScript:

When typing ...rest for an html child, React helpfully provides the HTMLAttributes and HTMLProps generics which you can provide with a specific HTML element type. Some html elements that only inherit global attributes, like <aside>, have no specific type, and can be typed with HTMLElement. If you’re using an IDE with good TypeScript support, you should find auto-completions for these types when typing HTML

It can be helpful to rename ...rest (e.g. ...divProps) to reflect which component the props are spread into.

// html child
import type { HTMLAttributes } from "react";

type Props = {
  colour: string;
} & HTMLAttributes<HTMLDivElement>;

const Car = ({ colour, ...divProps }: Props): JSX.Element => (
  <div {...divProps}>`My car is ${colour}.`</div>
);
// react child

// Wheels.tsx
export type Props = {
  spinners: boolean;
}

export const Wheels = ({ spinners }: Props): JSX.Element => {
  if (spinners) {
    return (
      <>My car is gangsta.</>
    )
  }
  
  return <>My car is boring.</>
};

// Car.tsx
import { Wheels } from "./Wheels";
import type { Props as WheelsProps } from "./Wheels";

type Props = {
  colour: string;
} & WheelsProps;

const Car = ({ colour, ...wheelProps }: Props): JSX.Element => (
  <p>
    `My car is ${colour}.`
    <Wheels {...wheelProps} />
  </p>
)

// In use
<Car colour="red" spinners={false} />
// renders => "<p>My car is red. My car is boring.</p>"

For a real world example of this, have a look at the Accordion component in react-components.

Other resources

React+Typescript Cheatsheets

Tooling

Build tools

Webpack is the preferred build tool for applications, and Rollup is preferred for libraries.

Rollup and Webpack for the most part have feature parity, so this is mostly a matter of taste and convenience, given Rollup is easier to configure for library builds.

Dependency management

We use Yarn for dependency management.

Packaging

Use the npm files array

Use the package.json files array to allowlist files for inclusion in your package. This is the safest approach, as it is explicit; using .npmignore is potentially dangerous, as you may end up packaging files unintentionally (credentials in a .env file being the worst case scenario!).

See Publishing what you mean to publish for further details.

Testing

We use the Jest unit test framework.

General Testing Advice

Testing React

  • Tests should be collocated with their component in the same directory (MyComponent.tsx should have a corresponding MyComponent.test.tsx)

  • If a component seems difficult to test, it might indicate that it needs to be further decomposed into smaller components.

  • Unittests should test component behaviour, rather than explicit rendering. Typically you want to test state changes,

that handlers are called with appropriate arguments, and any internal logic.

  • Jest snapshots should be avoided for complex outputs as they require updating any time a layout change is made even if there is no difference in functionality. A snapshots should be small and only used when testing the important output attributes is tedious or otherwise undesirable. Snapshot tests should compliment unit tests rather than replace them, as unit tests better describe the intended behaviour and output of a component.

  • Use the testing libraries mount methods over shallow where possible to ensure you’re testing the full component stack.

Redux
  • Connected components (redux containers) should be tested using redux-mock-store.

  • Containers that dispatch actions should test that component behaviours dispatch the expected actions.

  • redux-saga-test-plan is a helpful tool if you’re using sagas.

  • Reducers, action creators and selectors should all have simple functional tests.

  • Test composed Reselect selectors in isolation with resultFunc.

Code style & formatting

To ensure code formatting consistency across our codebase, we use Prettier.

Commenting code

As a general rule, human-readable code is preferred over brevity.

Inline comments should be provided where logic or behaviour is unexpected or required due to external factors. If not explicitly clear, adding a comment to answer the “Why did they do this” can greatly help debugging in the future.

If library code, JSDoc comments should be provided as appropriate.

React

Starting a new project

When starting a new React application, we recommend create-react-app for bootstrapping the project. Avoid ejecting as long as feasible to make updating dependencies easier.

We recommend new projects use the hooks API rather than class based components. Hooks generally make components simpler, allow reuse of stateful non-visual code, and perform better.

Preferred libraries

All projects should generally use the following:

If you require routing, or state management:

Hooks

For hooks based projects, we recommend the following:

  • Hooks based components should still follow the class based component’s lifecycle ordering:
  1. computation/logic functions (typically best outside the component)

  2. state (useState)

  3. side-effects (useEffect)

  4. event handlers

  5. render

  • Use the redux hooks API and consider connect deprecated.

  • Use ES6 default parameters instead of defaultProps.

  • For expensive functions passed down to child components, consider using the useCallback hook.

  • Hooks introduce some coupling which means you’ll need to mount redux connected components when writing enzyme tests (previously you could export the unconnected component).

Redux specific

  • Selector composition and memoisation - Reselect

  • Immutable state management - Immer

  • Async/side-effects management (http, localstorage etc.) - redux-saga

  • Redux toolset - Redux Toolkit

Reference projects

jaas-dashboard - project uses the hooks API and reflects current standards.

maas-ui - project uses the hooks API and reflects current standards.

crbs-ui (aka RBAC) - generally reflects current standards, but class based.

File naming conventions

  • Component files should use PascalCase and match the name of the default export (e.g. MyComponent.tsx).

  • Avoid the use of non-inclusive language such as: whitelist, blacklist. A full list of non-inclusive terms can be found here.

Variable/function naming conventions

  • Variables should use camelCase and describe their use (e.g. initialState)

  • Avoid the use of non-inclusive language such as: whitelist, blacklist. A full list of non-inclusive terms can be found here.

Redux

General Principles

Reducers

Summary

This text will be hidden

Selectors

  • Use selectors to access state in your components, ideally with Reselect which provides memoisation. Selectors can be elegantly combined to create different views of state.

Testing

  • Generally, it is best to test the component in its connected state, using redux-mock-store. In cases where you want to test the unconnected functionality of a connected component, it is okay to create a named export with a “Component” suffix. e.g. If your default export is export default connect(mapStateToProps, mapDispatchToProps)(UserList) you can also export export { UserList as UserListComponent } for testing.

References


Last updated 8 months ago.