Skip to content

UI Style Guide

Ariel Schwartz edited this page Jul 14, 2021 · 4 revisions

Superficial Style Preferences

Lint

Many of our superficial style preferences (indentation, line length, etc.) are enforced by eslint and a few vestiges of tslint. tslint is deprecated, so we are in the process of switching completely over to eslint.

Alphabetize

Unless there is a meaningful non-alphabetic order (e.g. schema order for columns in DAOs), alphabetize lists of constants, enum entries, etc.

Naming

Use lowerCamelCase for variables, functions, higher-order components, and props & state.

Use UpperCamelCase for classes, function components, enums.

Use SHOUTY_SNAKE_CASE for constants and enum entries.

Use kebab-case for directories and files.

General Principles

Alt Text

Use alt text on <img> tags, including on icons. A good guide to alt text for accessibility can be found here.

Code Comments

Try to make your code simple and easy to understand to prevent the need for comments. Use variable names that are clear and concise, without abbreviations. Keep functions small and minimize branching.

Include code comments for code whose function is not immediately obvious (e.g. 'how does this work' questions in code review, long sets of collection transforms).

Include code comments for code whose purpose is not immediately obvious (e.g. 'why does this exist' questions in code review).

When making TODO, FIXME, HACK, etc. comments, include a reference to a Jira ticket to complete / fix / untangle the piece of code necessitating said comment.

Declare Props as Interfaces

Rather than inlining, declare props as interfaces. This is considered easier to read, and in addition, the interfaces can then be used to type-check in e.g. function arguments.

interface Props {
  name: string;
}

export const ExampleComponent = (props: Props) => {doThing}

lodash/fp

Use lodash/fp for common collection/object operations such as map, filter, reduce, etc. lodash/fp is a functional programming version of lodash that swaps its arguments such that the iteratee(s) comes first and the data comes last, and that auto-curries all methods:

// lodash syntax
_.map([1, 2, 3], (n) => n * n);

// lodash/fp syntax
fp.map((n) => n * n)([1, 2, 3]);

Functional code / const

Use const to declare variables wherever possible. Exceptions include unit tests, where we commonly modify a let variable scoped to the test and utils where we modify or format strings.

Exceptions may include building a collection or object that we gradually or optionally add contents to. Consider using fp.fold, possibly in combination with fp.flow and fp.filter. Use your judgment about which is more reasonable.

When you do not use const to declare variables, use let, which is block-scoped, instead of var, which is function-scoped.

Use const to declare functions as well:

// Preferred
const doThing = (argument) => {
  return [...]
}

// Not preferred
function doThing(argument) {
  return [...]
}

Prefer One Component Per File

Most components that return JSX should be in their own file. Exceptions include nested items that are only used in one place and categories of similar higher-order-components.

Typing

Provide types for function arguments, especially when non-obvious such as when using fp.flow.

This can be overly verbose and/or difficult when passing a function as an argument. It is acceptable to define the type as Function in this case.

Use reactStyles

When a style is shared between two or more elements in a file, we commonly pull it out to the top of the file to a styles object so it doesn't have to be written out multiple times. When we do this, use reactStyles to make sure that all CSS properties are valid.

The initial motivation for this was a Typescript bug that is now fixed. We haven't upgraded Typescript to version 3.4 yet because it is not compatible with Angular. When the React conversion is complete, we can deprecate reactStyles.

State

Use Function Components

Use function components with hooks to manage state instead of using class components wherever possible.

interface Props {
  name: string;
}

export const ExampleComponent = (props: Props) => {
  const [loading, setLoading] = useState();
  return <React.Fragment>
    {loading && <Spinner/>}
    Hello world!
    // TODO: something that makes loading turn on and off
  </React.Fragment>
}

If you have to modify a class component and it can be converted into a function component without completely blowing up the scope of the ticket, convert it.

For Existing Class Components

For the same reasons as above in Declare Props as Interfaces, declare state as an interface.

interface Props {
  name: string;
}

interface State {
  loading: boolean;
}

export class ExampleComponent extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
  }
}

If you will be reusing the Props and State interfaces for a class component, namespace them:

export interface ExampleProps {
  name: string;
}

export interface ExampleState {
  loading: boolean;
}

export class ExampleComponent extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
  }
}

Global State

We prefer to minimize our use of global state, but still have a moderate amount of it. We use a combination of rxjs Observable/ReplaySubject and handrolled atom singletons to manage global state. rxjs was initially introduced to interface with Angular dependency injection, so atom is preferred.

Use Hooks and Higher-Order Components For Shared Logic

We have many of these in index.tsx for pulling in information about the logged in user's profile, the currently viewed workspace, etc. We also do this for withSuccessModal and withErrorModal.