TS-41: React

This technical standard covers best practices for working with React, a popular JavaScript library used for composing graphical web user interfaces from reusable components.

Importing React

From React v17, the explicit import of React is no longer required.

import React from "react"

Hooks and other utilities can be imported from the react package individually.

/* React ≤ v17 */
import React, { useState } from "react"

/* React ≥ v18 */
import { useState } from "react"

/* Usage: */
const [things, setThings] = useState([])

But it is RECOMMENDED to import the entire react package, and use the React namespace for all React-related imports. This makes it easier to identify React-related code in your project.

import React from "react"

/* Usage: */
const [things, setThings] = React.useState([])

For this reason, the import React from "react" pattern is still RECOMMENDED for all React components.

Wrapping native elements

It is RECOMMENDED to use the spread operator to pass all props from React components to the underlying native elements they wrap.

import React from "react"

export default function Button(props) {
  return (
    <button {...props}>
      {props.children}
    </button>
  )
}

This allows users of the component to set any number of native HTML attributes as props. This pattern means that consumers could pass invalid props, which are not supported by the abstracted native element. However, since HTML will just ignore invalid attributes, this will not cause any runtime issues. It’s a good trade-off for the increased flexibility.

An improvement to this pattern is to use object destructuring on props. This allows you to cherry pick props that fulfill component-specific roles, while passing the rest of the props to the underlying native element.

import React from "react"

export default function Button({children, className, size, ...rest}) {
  return (
    let sizeClass
    if (size === "sm") sizeClass = "button-small"
    if (size === "lg") sizeClass = "button-large"

    <button className={`${sizeClass} ${className}`} {...rest}>
      {children}
    </button>
  )
}

This pattern simplifies the code (components don’t need to explicitly plan for every possible attribute that a native HTML element supports) and increases reusability (you can use the React components for every use case you would be able to if you were writing pure HTML).

This pattern is restricted to wrappers for single native HTML elements. It is NOT RECOMMENDED to use this pattern for custom components, composed of multiple native elements and/or other components. In this case, the props are the part of the component’s declarative API, and should be explicitly defined.

State

State setters in React SHOULD NOT directly pass new state values, but SHOULD instead use the callback API, which receives the current state value as its argument and which is expected to return the new state value. Although this is more verbose, it is best practice because it ensures that the new state value is based on the most recent state value.

export default function App() {
  const [count, setCount] = React.useState(0)

  /* No: */
  function add() {
    setCount(count++)
  }

  /* Yes: */
  function subtract() {
    setCount(prevCount => prevCount - 1)
  }

  return (
    <div className="counter">
      <button className="counter--minus" onClick={subtract}>–</button>
      <div className="counter--count">
        <h1>{count}</h1>
      </div>
      <button className="counter--plus" onClick={add}>+</button>
    </div>
  )
}

Filesystem

React components should be organized in a way that reflects the filesystem structure of the project. This means that components should be placed in directories that correspond to the component hierarchy.

File extensions

Since JSX is not standard JavaScript, source files that contain JSX MUST use the .jsx file extension rather than .js. Likewise, TypeScript source files that contain JSX MUST use the .tsx extension rather than .ts.

Note

Compilers do not distinguish between .js and .jsx (or between .ts and .tsx) — the extension is a convention for the benefit of people reading the code, not a requirement of the toolchain. Using the JSX-specific extension makes it immediately clear which files contain JSX syntax.

Filesystem structure

The following filesystem structure is RECOMMENDED as a starting point. In this design:

  • Elements are the smallest building blocks of the application, such as buttons, form fields, and icons. This category also includes larger composite elements that are still generic and reusable, such as a calendar, data table, or modal dialog.
  • Features are libraries of domain-oriented components that are specific to the application, such as payment management, transaction history, and user authentication. Feature components are typically composed of multiple elements and may include their own state management, data fetching, and business logic.
  • Views are the top-level components that represent the different screens or views of the application. They are composed of multiple features and elements, and are responsible for rendering the overall layout and structure of the application. They should not contain any domain-specific logic, which should be encapsulated within the features. But they may contain UI logic, such as routing and navigation.
src/
├── assets/
├── elements/
│   ├── button/
│   └── calendar/
│       ├── components/
│       ├── types/
│       └── utils/
├── features/
│   ├── payments/
│   │   ├── components/
│   │   │   ├── PaymentForm.tsx
│   │   │   ├── PaymentList.tsx
│   │   │   └── PaymentDetails.tsx
│   │   ├── hooks/
│   │   ├── mutations/
│   │   ├── queries/
│   │   ├── services/
│   │   ├── types/
│   │   ├── utils/
│   │   └── index.ts
│   ├── transactions/
│   │   ├── components/
│   │   ├── ...
│   │   └── index.ts
│   └── auth/
│       ├── components/
│       ├── queries/
│       └── index.ts
├── views/
│   ├── components/
│   ├── queries/
│   ├── mutations/
│   ├── hooks/
│   ├── services/
│   ├── types/
│   └── utils/
└──tests/

Tests MAY be colocated with the components they test (for purer modularity), or placed in a separate tests/ directory. Generally, collocating tests is RECOMMENDED for large, complex projects, while a dedicated top-level tests/ directory tends to work well only for smaller projects.

Folder rules

Category

File/Folder naming

Examples

API

camelCase

registerAlert.ts (HTTP POST opertion)
getAlerts.ts (HTTP GET operation)

Queries

camelCase

treeReasonByParentIdQuery.ts (GraphQL query)

Components

PascalCase

Ticket.tsx
TicketList.tsx TicketForm.tsx

Constants

kebab-case

query-keys.ts (query keys)
alert.constants.ts (constants related to the feature)

Contexts

PascalCase

AlertsContext.tsx (feature context)

Hooks

camelCase

useAlertMutation.ts (useMutation implementation)
useAlertsQuery.ts (useQuery implementation)

Views

PascalCase

AlertScreen.tsx (feature view)

Utils

kebab-case

alert.utils.ts (utility functions related to the feature)

Types

kebab-case

alert.types.ts (types related to the feature)