-
Notifications
You must be signed in to change notification settings - Fork 10
UI Style Guide
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.
Unless there is a meaningful non-alphabetic order (e.g. schema order for columns in DAOs), alphabetize lists of constants, enum entries, etc.
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.
Use alt text on <img>
tags, including on icons. A good guide to alt text for accessibility can be found here.
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.
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}
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]);
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 [...]
}
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.
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.
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
.
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 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);
}
}
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.
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
.