Skip to main content

React Style Guide

Current as of July 10, 2022

Introduction

This guide is based on the Airbnb React/JSX Style Guide with minor adjustments to fit our implementations.

Note: When ever possible there should be an eslint rule enforcing the styles discussed in this guide.

Basic Rules

  • Only include one React component per file.
    • However, multiple Stateless, or Pure, Components are allowed per file. eslint: react/no-multi-comp.
  • Always use TSX syntax.

Hooks and Function Components over Class Components

Regardless of whether or not the component has state, it is encouraged to always use function components over Class components except in rare instances where we need access to underlying lifecycle methods unavailable to hooks.

If your component does have state, consider making use of the React provided useState hook.

To perform any side effects, such as the React class lifecycle methods, make use of the useEffect hook.

Custom Hooks

When you write stateful logic with hooks that you need to duplicate in another component, or you need to implement logic already present in another component, follow the DRY principle and extract it into a more generic "custom" hook. Components using a shared custom hook don't actually share state because all state and effects inside of each call are isolated, similar to using React hooks like useState across components.

info

Read the React documentation on custom hooks for more explanations and examples.

For example, we have multiple components in our codebase that need to execute something on escape keypress, like hiding a modal. We wrote the logic to "dismiss" a modal on keypress like so...

// ... component setup and creation of a function called `onDismiss` that utilizes `useState` ...

useEffect(() => {
const handler = ({ key }: KeyboardEvent) => {
if (key === 'Escape') {
onDismiss();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [onDismiss]);

This calls the function onDismiss when the 'Escape' key is pressed. Because we later had multiple modals and other components that we want to execute a function on 'Escape' keypress, to avoid copying and pasting this logic into every component it was extracted into lib/hooks.tsx with a slightly more generic function name to be executed:

export function useEscKeydownEffect(onEscKeydown: Function) {
useEffect(() => {
const handler = ({ key }: KeyboardEvent) => {
if (key === 'Escape') {
onEscKeydown();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [onEscKeydown]);
}

Now each component can define its own function to be executed on this keypress, and we can easily use it across our components by passing in that function:

// ... component setup and creation of a function called `onDismiss` that utilizes `useState` ...

useEscKeydownEffect(onDismiss);

Custom hook extraction also eliminates duplicate tests since tests can be written for the hook instead of testing similar logic across various components.

If hooks.tsx grows to be too large, consider storing them in a hooks/ directory instead.

Mixins

Do not use mixins.

Why? Mixins introduce implicit dependencies, cause name clashes, and cause snowballing complexity. Most use cases for mixins can be accomplished in better ways via components, higher-order components, or utility modules.

Models

In settings we have multiple models that we interact with. These models provide the following abstractions:

  • Provide a way to access data stored elsewhere ie, local storage, query parameters, or even just in memory.
  • Ensure that data is of a valid format
  • Allows us to group data into logic blocks to hopefully making application state easier to understand.
  • Allows us to document access and usage of data
  • Allows us to reuse logic for accessing data.

Models can be found in fxa-settings the src/models folder, and always take a form like the following. (ClientInfo is a good example since it’s fairly small and illustrates all the key abstractions)

import {
IsOptional,
IsString,
IsBoolean,
IsHexadecimal,
} from 'class-validator';
import {
bind,
KeyTransforms as T,
ModelDataProvider,
} from '../../lib/model-data';


export class ClientInfo extends ModelDataProvider {
@IsOptional()
@IsHexadecimal()
@bind('id')
clientId: string | undefined;


// TODO - Validation - Needs @IsEncodedUrl()
@IsOptional()
@IsString()
@bind(T.snakeCase)
imageUri: string | undefined;


@IsOptional()
@IsString()
@bind('name')
serviceName: string | undefined;


// TODO - Validation - Needs @IsEncodedUrl()
@IsOptional()
@IsString()
@bind(T.snakeCase)
redirectUri: string | undefined;


@IsOptional()
@IsBoolean()
@bind()
trusted: boolean | undefined;
}

A few key points here:

  • Models always derive from ModelDataProvider. This is the base class that provides the magic for driving the @bind() decorators. More on bind decorators below.
  • The class-validator decorators are used to ensure data integrity. More on class validators below.
  • The bind decorator should always go last as the decorators are executed in reverse order.
  • Multiple class validators can be applied.

Here's an example of creating a model instance:

const data = new GenericData({});
const model = new ClientInfo(data);

A couple things to note about creating models:

  • Models always wrap a ‘data-store’. The data store is exactly as it sounds. It’s simply a class that provides data storage. All data stores inherit from the ModelDataStore class.
  • At the time of writing we have several ModelDataStore implementations. They support storing data in UrlQueryParameters, LocalStorage, or in an object in memory.
  • Creating new ModelDataStores should be pretty straightforward. Simply follow the patter defined by existing ones.

Bind Decorators

The @bind() decorator is something we created for binding a field to data in the ModelDataStore that was provided when the model instance was instantiated. By default, with no arguments, the bind decorator simply looks up a key that correlates to the field name.

For example, trusted would map to a ‘trusted’ field in the data store.

const data = new GenericData({ trusted: true });
const model = new ClientInfo(data);
console.log(model.trusted);
// outputs: true

The bind decorator can also be instructed to bind to a specific field by bassing in string. Again this is most easily understood through example. In this case, the clientId field has @bind('id'), for its bind decorator.

data = new GenericData({ id: ‘123’ });
model = new ClientInfo(data);
console.log(model.clientId);
// outputs: 123

Another way of customizing the bind decorator is through a ‘transform’ rule. A transform is simply a function that takes the field name, and converts a field name into a key name. For example, here are the two transforms we currently use:

export const KeyTransforms = {
snakeCase: (k: string) =>
k
.split(/\.?(?=[A-Z])/)
.join('_')
.toLowerCase(),
lower: (k: string) => k.toLowerCase(),
};

The redirectUri uses the ‘snakeCase’ transform for redirectUri. So for example:

const data = new GenericData({ redirect_uri: ‘mozilla.com’ });
const model = new ClientInfo(data);
console.log(model.clientId);
// outputs: mozilla.com

Class Validators Decorators

We ensure class validation by using class-validator decorators. This is a commonly used npm package, so there’s no point in re-documenting the functionality in this package, but there are a couple notes to make:

  • Works with bind decorator. Put the bind decorator last!
  • Custom validators are pretty easy to create.
  • The Decorators should be aligned with the field type.
  • The checks are applied on write and read. This means if you try to access an invalid value, or try to write an invalid value an error will occur.
  • If a validation error occurs, we can see the offending property name in the error state. It'll be held under 'property'.
  • More often than not we need the @IsOptional(), since it’s likely that model data won’t be present 100% of the time.

Naming

This is a copy of what can be found in the Airbnb styleguide.

  • Extensions: Use .tsx extension for React components.
  • Filename: Use PascalCase for filenames. E.g., ReservationCard.tsx.
  • Reference Naming: Use PascalCase for React components and camelCase for their instances. eslint: react/jsx-pascal-case
// bad
import reservationCard from './ReservationCard';

// good
import ReservationCard from './ReservationCard';

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;
  • Component Naming: Use the filename as the component name. For example, ReservationCard.tsx should have a reference name of ReservationCard. However, for root components of a directory, use index.tsx as the filename and use the directory name as the component name:
// bad
import Footer from './Footer/Footer';

// bad
import Footer from './Footer/index';

// good
import Footer from './Footer';
  • Higher-order Component Naming: Use a composite of the higher-order component’s name and the passed-in component’s name as the displayName on the generated component. For example, the higher-order component withFoo(), when passed a component Bar should produce a component with a displayName of withFoo(Bar).
    • Why? A component’s displayName may be used by developer tools or in error messages, and having a value that clearly expresses this relationship helps people understand what is happening.
// bad
export default function withFoo(WrappedComponent) {
return function WithFoo(props) {
return <WrappedComponent {...props} foo />;
}
}

// good
export default function withFoo(WrappedComponent) {
function WithFoo(props) {
return <WrappedComponent {...props} foo />;
}

const wrappedComponentName = WrappedComponent.displayName
|| WrappedComponent.name
|| 'Component';

WithFoo.displayName = `withFoo(${wrappedComponentName})`;
return WithFoo;
}
  • Props Naming: Avoid using DOM component prop names for different purposes.
    • Why? People expect props like style and className to mean one specific thing. Varying this API for a subset of your app makes the code less readable and less maintainable, and may cause bugs.
// bad
<MyComponent style="fancy" />

// bad
<MyComponent className="fancy" />

// good
<MyComponent variant="fancy" />

Alignment

  • Follow these alignment styles for JSX syntax. eslint: react/jsx-closing-bracket-location react/jsx-closing-tag-location
// bad
<Foo superLongParam="bar"
anotherSuperLongParam="baz" />

// good
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
/>

// if props fit in one line then keep it on the same line
<Foo bar="bar" />

// children get indented normally
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
>
<Quux />
</Foo>

// bad
{showButton &&
<Button />
}

// bad
{
showButton &&
<Button />
}

// good
{showButton && (
<Button />
)}

// good
{showButton && <Button />}

// good
{someReallyLongConditional
&& anotherLongConditional
&& (
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
/>
)
}

// good
{someConditional ? (
<Foo />
) : (
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
/>
)}

Quotes

  • Always use double quotes (") for JSX attributes, but single quotes (') for all other JS. eslint: jsx-quotes
    • Why? Regular HTML attributes also typically use double quotes instead of single, so JSX attributes mirror this convention.
// bad
<Foo bar='bar' />

// good
<Foo bar="bar" />

// bad
<Foo style={{ left: "20px" }} />

// good
<Foo style={{ left: '20px' }} />

Spacing

  • Always include a single space in your self-closing tag. eslint: no-multi-spaces, react/jsx-tag-spacing
// bad
<Foo/>

// very bad
<Foo />

// bad
<Foo
/>

// good
<Foo />
  • Do not pad JSX curly braces with spaces. eslint: react/jsx-curly-spacing
// bad
<Foo bar={ baz } />

// good
<Foo bar={baz} />

Props

  • Always use camelCase for prop names.
// bad
<Foo
UserName="hello"
phone_number={12345678}
/>

// good
<Foo
userName="hello"
phoneNumber={12345678}
/>
  • Omit the value of the prop when it is explicitly true. eslint: react/jsx-boolean-value
// bad
<Foo
hidden={true}
/>

// good
<Foo
hidden
/>

// good
<Foo hidden />
  • Always include an alt prop on <img> tags. If the image is presentational, alt can be an empty string or the <img> must have role="presentation". eslint: jsx-a11y/alt-text
// bad
<img src="hello.jpg" />

// good
<img src="hello.jpg" alt="Me waving hello" />

// good
<img src="hello.jpg" alt="" />

// good
<img src="hello.jpg" role="presentation" />
  • Do not use words like “image”, “photo”, or “picture” in <img> alt props. eslint: jsx-a11y/img-redundant-alt
    • Why? Screenreaders already announce img elements as images, so there is no need to include this information in the alt text.
// bad
<img src="hello.jpg" alt="Picture of me waving hello" />

// good
<img src="hello.jpg" alt="Me waving hello" />
  • Use only valid, non-abstract ARIA roles. eslint: jsx-a11y/aria-role
// bad - not an ARIA role
<div role="datepicker" />

// bad - abstract ARIA role
<div role="range" />

// good
<div role="button" />
  • Do not use accessKey on elements. eslint: jsx-a11y/no-access-key
    • Why? Inconsistencies between keyboard shortcuts and keyboard commands used by people using screenreaders and keyboards complicate accessibility.
// bad
<div accessKey="h" />

// good
<div />
  • Avoid using an array index as key prop, prefer a stable ID. eslint: react/no-array-index-key
    • Why? Not using a stable ID is an anti-pattern because it can negatively impact performance and cause issues with component state.
// bad
{todos.map((todo, index) =>
<Todo
{...todo}
key={index}
/>
)}

// good
{todos.map(todo => (
<Todo
{...todo}
key={todo.id}
/>
))}
  • Use spread props sparingly.

Exceptions:

  • Spreading objects with known, explicit props. This can be particularly useful when testing React components with Mocha’s beforeEach construct.
export default function Foo {
const props = {
text: '',
isPublished: false
}

return (<div {...props} />);
}

Notes for use: Filter out unnecessary props when possible. Also, use prop-types-exact to help prevent bugs.

// good
function Example(props) {
const { irrelevantProp, ...relevantProps } = props;
return <WrappedComponent {...relevantProps} />
}

// bad
function Example(props) {
return <WrappedComponent {...props} />
}
  • Whenever we pass a value in via a prop that matches the name of the prop, we should prefer {...{ propName }} syntactic sugar. It looks nicer, is more readable, and is slightly easier to update later.
// bad
<MyComponent foo={foo} />

// good
<MyComponent {...{ foo }} />

// bad
<MyComponent foo={foo} bar={bar} baz={baz} />

// good
<MyComponent {...{ foo, bar, baz }} />
  • Whenever we render a className on a component, or really anything that's going to cause a property to render on a DOM element, we should NOT make the default value an empty string anywhere in the component tree, but it should be undefined instead. This is because those properties will render on those DOM elements unnecessarily.
// bad
<div className="" /> // renders <div class=" "></div>

// good
<div className={undefined} /> // renders <div></div>

Refs

Pretty much use what Airbnb has. Are there other things we can include?

  • Always use ref callbacks. eslint: react/no-string-refs
// bad
<Foo
ref="myRef"
/>

// good
<Foo
ref={(ref) => { this.myRef = ref; }}
/>

Parentheses

  • Wrap JSX tags in parentheses when they span more than one line. eslint: react/jsx-wrap-multilines
// bad
function Example() {
return <MyComponent variant="long body" foo="bar">
<MyChild />
</MyComponent>;
}

// good
function Example() {
return (
<MyComponent variant="long body" foo="bar">
<MyChild />
</MyComponent>
);
}

// good, when single line
function Example() {
const body = <div>hello</div>;
return <MyComponent>{body}</MyComponent>;
}

Wrapping

Use a React fragment, e.g. <></>, instead of a generic element wrapper like <div> when returning elements that don't need a DOM wrapper to prevent unnecessary DOM elements from being rendered

// bad
function Example() {
return (
<div>
<h1>Foo</h1>
<p>Bar</p>
</div>
)
}

// good
function Example() {
return (
<>
<h1>Foo</h1>
<p>Bar</p>
</>
)
}

Tags

  • Always self-close tags that have no children. eslint: react/self-closing-comp
// bad
<Foo variant="stuff"></Foo>

// good
<Foo variant="stuff" />
  • If your component has multi-line properties, close its tag on a new line. eslint: react/jsx-closing-bracket-location
// bad
<Foo
bar="bar"
baz="baz" />

// good
<Foo
bar="bar"
baz="baz"
/>

Methods

  • Use arrow functions to close over local variables. It is handy when you need to pass additional data to an event handler. Although, make sure they do not massively hurt performance, in particular when passed to custom components that might be PureComponents, because they will trigger a possibly needless rerender every time.
function ItemList(props) {
return (
<ul>
{props.items.map((item, index) => (
<Item
key={item.key}
onClick={(event) => { doSomethingWith(event, item.name, index); }}
/>
))}
</ul>
);
}

Ordering

Ordering for function components.

  1. State variables using the useState hook
  2. Side effects using the useEffect hook
  3. Render

Import organizing

Your file imports should be organized using the organizeImports feature of the TypeScript language service API via prettier-plugin-organize-imports

Context

React Context provides a way to make certain state or prop values "global" to React applications or to portions of React apps. This is most commonly used when deeply nested components are present and "prop drilling", or passing props through many intermediate elements, is a maintenance and development burden. Prop drilling can be alleviated through a pattern of Context "providers" which provide access to data stored in Context objects that are then consumed by components that need the data, skipping prop drilling from intermediate elements.

For example, we could have one AppContext object for app-global data and add user data into it. If an <AppContext.Provider> is wrapped around entire application, any component should be able to consume that user data like const { uid } = useContext(AppContext). We could also have a different provider wrapping only a portion of the application.

We can also abstract away pulling in useContext and AppContext for all consumers of AppContext by creating a simple custom hook to do this for us. For example, in Settings, we created a custom hook like so:

export function useAccount() {
const { account } = useContext(AppContext);
if (!account) {
throw new Error('Are you forgetting an AppContext.Provider?');
}
return account;
}

This allows us to more simply pull in useAccount and retrieve data with const { uid } = useAccount(); in consuming components, and additionally gives developers a clue when the expected value isn't present which is especially useful in testing.

Pitfalls

Using Context isn't free. Every time the value of a Context object changes, any consumers of that component will rerender. Therefore, it should be used for pieces of data that update infrequently.

New Context Providers should be created with thoughtfulness to the "Before You Use Context" section of the React docs.

Additionally, consider consuming Context values in a parent component and passing those values down one level into child components rather than calling useContext in every child component which can be heavy.