3

React is a JavaScript library that is the most popular and industry-leading front-end development library today.

JavaScript is a loosely typed language, so it captures the runtime. The result of this is that JavaScript errors are caught very late, which can lead to serious bugs.

Of course React, as a JavaScript library, also inherits this problem.

Clean code ( ") is a consistent style of programming that makes code easier to write, read, and maintain. Anyone can write code that a computer can understand, but good developers can write code that humans can Understandable clean code.

Clean code is a reader-centric development style that improves the quality and maintainability of our software.

Writing clean code requires writing code with clear and simple design patterns that make it easy for people to read, test, and maintain the code. Therefore, clean code can reduce the cost of software development. This is because of the principles involved in writing clean code, eliminating technical debt.

In this article, we'll cover some useful patterns to use when working with React and TypeScript.

💡 To make it easier for your team to keep code healthy and prioritize technical debt work, try the VS Code and JetBrains extensions with Stepsize. They help engineers create technical problems, add them to iterations, and continuously resolve technical debt—all without leaving the editor.

Now let's take a look at 10 useful patterns to apply when using React and Typescript:

1. Import React using default imports

Consider the following code:

import * as React from "react";

While the above code works, if we don't use everything React, it's confusing and not a good practice to import them. A better pattern is to use a default export like this:

import React, {useContext, useState} from "react";

Using this approach, we can deconstruct what we need from the React module instead of importing everything.

Note: To use this option, we need to configure tsconfig.json file as follows:

{
  "compilerOptions": {
    "esModuleInterop": true"
  }
}

In the code above, by setting esModuleInterop to true, we enable allowSyntheticDefaultImports , which is very important for TypeScript to support our syntax.

2. Type declares before runtime implementation

Consider the following code:

import React, {Component} from "react";

const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }

type State = typeof initialState;
type Props = { count?: number } & typeof defaultProps

class Counter extends Component {

   static defaultProps = defaultProps;
   state = initialState;

   // ...

}

The code above can be cleaner and more readable if we separate the runtime declarations from the compile-time declarations, and the compile-time declarations come before the runtime declarations.

Consider the following code:

import React, {Component} from "react";

type State = typeof initialState;
type Props = { count?: number } & typeof defaultProps

const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }

class Counter extends Component {

   static defaultProps = defaultProps;
   state = initialState;

   // ...

}

Now, at first glance, the developer knows what the component API looks like, because the first line of code clearly shows that.

Also, we separate compile-time declarations from runtime declarations.

3. Provide explicit props to children

Typescript mirrors how React handles children props by annotating them as optional for function and class components in react.d.ts .

Therefore, we need to explicitly provide a props type for children . However, it is always best to always annotate the props of children explicitly with a type. This is useful in cases where we want to use children for content projection, and if our component doesn't use it, we can simply annotate it with the never type.

Consider the following code:

import React, {Component} from "react";
// Card.tsx
type Props = {
    children: React.ReactNode
}

class Card extends Component<Props> {
    render() {
        const {children} = this.props;
        return <div>{children}</div>;
    }
}

Here are some props types for annotation children :

  • ReactNode | ReactChild | ReactElement
  • For primitive types you can use: string | number | boolean
  • Objects and arrays are also valid types
  • never | null | undefined - Note: null and undefined

4. Use type inference to define component state or DefaultProps

See the code below:

import React, {Component} from "react";

type State = { count: number };

type Props = {
    someProps: string & DefaultProps;
}

type DefaultProps = {
    name: string
}

class Counter extends Component<Props, State> {
    static defaultProps: DefaultProps = {name: "John Doe"}
    state = {count: 0}

    // ...
}

While the above code works, we can make the following improvements to it: Enable TypeScript's type system to correctly infer readonly types, like DefaultProps and initialState .

To prevent development errors due to accidentally setting state: this.state = {}

Consider the following code:

import React, {Component} from "react";

const initialState = Object.freeze({ count: 0 })
const defaultProps = Object.freeze({name: "John Doe"})

type State = typeof initialState;
type Props = { someProps: string } & typeof defaultProps;

class Counter extends Component<Props, State> {
    static readonly defaultProps = defaultProps;
    readonly state  = {count: 0}

    // ...
}

In the code above, by freezing DefaultProps and initialState , the TypeScript type system can now infer them as type readonly .

Also, by marking the static defaultProps and the state as readonly in the class, we eliminate the above mentioned possibility of setting the state causing a runtime error.

5. Use type aliases when declaring Props/State, not interfaces

While type can be used, interface is preferred for consistency and clarity, as there are cases where interface will not work. For example, in the previous example, we refactored the code to enable TypeScript's type system to correctly infer readonly type by defining the state type from the implementation. We can't use interface in this mode like the code below:

// works
type State = typeof initialState;
type Props = { someProps: string } & typeof defaultProps;

// throws error
interface State = typeof initialState;
interface Props = { someProps: string } & typeof defaultProps;

Also, we cannot extend interface with types created by union and intersection, so we must use type in these cases.

6. Don't use method declarations in interface/type

This ensures schema consistency in our code because all members inferred by type/interface are declared the same way. Also, --strictFunctionTypes only works when comparing functions, not methods. You can get further explanation from this TS question.

// Don't do
interface Counter {
  start(count:number) : string
  reset(): void
}

// Do
interface Counter {
  start: (count:number) => string
  reset: () => string
}

7. Don't use FunctionComponent

Or FC for short to define a function component.

When using Typescript and React, functional components can be written in two ways:

  1. Like a normal function, like the following code:
type Props = { message: string };

const Greeting = ({ message }: Props) => <div>{message}</div>;
  1. Use React.FC or React.FunctionComponent like this:
import React, {FC} from "react";

type Props = { message: string };

const Greeting: FC<Props> = (props) => <div>{props}</div>;

Using FC provides advantages such as type checking and autocompletion for static properties such as displayName , propTypes , and defaultProps . But it has a known problem of breaking defaultProps and other attributes: propTypes , contextTypes , displayName .

FC also provides an implicitly typed children attribute, which also has known issues. Also, as discussed earlier, the component API should be explicit, so an implicitly typed children attribute is not the best.

8. Don't use constructors for class components

With the new class attribute proposal, it is no longer necessary to use constructors in JavaScript classes. Using the constructor involves calling super () and passing props , which introduces unnecessary boilerplate and complexity.

We can write cleaner and more maintainable React class components, using class fields like this:

// Don't do
type State = {count: number}
type Props = {}

class Counter extends Component<Props, State> {
  constructor(props:Props){
      super(props);
      this.state = {count: 0}
  }
}

// Do
type State = {count: number}
type Props = {}

class Counter extends Component<Props, State> {
  state = {count: 0}
}

In the code above, we see that using class attributes involves less boilerplate, so we don't have to deal with this variable.

9. Don't use the public keyword in classes

Consider the following code:

import { Component } from "react"

class Friends extends Component {
  public fetchFriends () {}
  public render () {
    return // jsx blob
  }
}

Since all members in the class are public by default and at runtime, there is no need to add extra boilerplate by explicitly using the public keyword. Instead, use the following pattern:

import { Component } from "react"

class Friends extends Component {
  fetchFriends () {}
  render () {
    return // jsx blob
  }
}

10. Don't use private in component classes

Consider the following code:

import {Component} from "react"

class Friends extends Component {
  private fetchProfileByID () {}

  render () {
    return // jsx blob
  }
}

In the code above, private only privatizes the fetchProfileByID method at compile time because it's just a Typescript mock. However, at runtime, the fetchProfileByID method is still public.

There are different ways to make properties/methods of a JavaScript class private, using the underscore (\_) variable naming guidelines are as follows:

import {Component} from "react"

class Friends extends Component {
  _fetchProfileByID () {}

  render () {
    return // jsx blob
  }
}

While this doesn't really make the fetchProfileByID method private, it does a good job of conveying to other developers our intent that the specified method should be considered private. Other techniques include using WeakMaps, Symbols, and scoped variables.

But with the new ECMAScript class field proposal, we can achieve this easily and elegantly by using private fields like this:

import {Component} from "react"

class Friends extends Component {
  #fetchProfileByID () {}

  render () {
    return // jsx blob
  }
}

And TypeScript supports the new JavaScript syntax for private fields in version 3.8 and above.

Additional: don't use enum

Although enum is a reserved word in JavaScript, using enum not a standard idiomatic JavaScript pattern.

But if you're using a language like C# or JAVA, it can be tempting to use an enum. However, there are better patterns like using compiled type literals like this:

// Don't do this
enum Response {
  Successful,
  Failed,
  Pending
}

function fetchData (status: Response): void => {
    // some code.
}

// Do this
type Response = Sucessful | Failed | Pending

function fetchData (status: Response): void => {
    // some code.
}

Summarize

Undoubtedly, using Typescript adds a lot of extra boilerplate to your code, but the benefits are well worth it.

To make your code cleaner and better, don't forget to implement a robust TODO/issue procedure. It will help your engineering team gain visibility into technical debt, collaborate on codebase issues, and better plan sprints.

This article is translated from: https://dev.to/alexomeyer/10-must-know-patterns-for-writing-clean-code-with-react-and-typescript-1m0g


leexiaoran
2k 声望1k 粉丝