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
andundefined
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:
- Like a normal function, like the following code:
type Props = { message: string };
const Greeting = ({ message }: Props) => <div>{message}</div>;
- 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。