React is a JavaScript library that is the most popular and industry-leading front-end development library today.
JavaScript is a loosely typed language and, as such, captures the runtime. The result of this is that JavaScript errors are caught very late, which can lead to serious bugs.
React, of course, inherits this problem as a JavaScript library.
Clean Code is a consistent programming style that makes code easier to write, read, and maintain. Anyone can write code that a computer can understand, but good developers can write clean code that humans can understand.
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 to read, test, and maintain code. Therefore, clean code can reduce the cost of software development. This is because the principles involved in writing clean code eliminate technical debt.
In this article, we’ll introduce some useful patterns to use with React and TypeScript.
💡 To make it easier for your team to keep Code healthy and prioritize technical debt work, try using Stepsize’s VS Code and JetBrains extensions. They help engineers create technical problems, add them to iterations, and continually resolve technical debt — without leaving the editor.
Now let’s take a look at 10 useful patterns to apply with React and Typescript:
1. Use the default import to import React
Consider the following code:
import * as React from "react";
Copy the code
While the above code works, importing React would be confusing and not a good practice if we didn’t use everything. A better pattern is to use the default export as shown below:
import React, {useContext, useState} from "react";
Copy the code
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 the tsconfig.json file, as shown below:
{
"compilerOptions": {
"esModuleInterop": true"}}Copy the code
In the code above, by esModuleInterop set to true, we have enabled allowSyntheticDefaultImports, the grammar is very important for the TypeScript support us.
Type 2.The statementBefore implementation at runtime
Consider the following code:
import React, {Component} from "react";
const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }
type State = typeofinitialState; type Props = { count? : number } &typeof defaultProps
class Counter extends Component {
static defaultProps = defaultProps;
state = initialState;
// ...
}
Copy the code
The above code can be clearer and more readable if we separate the runtime declaration from the compile-time declaration, and the compile-time declaration precedes the runtime declaration.
Consider the following code:
import React, {Component} from "react";
type State = typeofinitialState; type Props = { count? : number } &typeof defaultProps
const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }
class Counter extends Component {
static defaultProps = defaultProps;
state = initialState;
// ...
}
Copy the code
Now, at first glance, the developer knows what the component API looks like, because the first line of code clearly shows it.
In addition, we separate compile-time declarations from runtime declarations.
3. Give children definite props
Typescript reflects how React handles children props by annotating them as optional for function components and class components in react.d.ts.
Therefore, we need to explicitly provide a props type for children. However, it is always a good idea to comment the children props explicitly with 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>; }}Copy the code
Here are some props types for annotating children:
ReactNode | ReactChild | ReactElement
- For primitive types you can use:
string | number | boolean
- Objects and arrays are also valid types
never | null | undefined
– Note: This parameter is not recommendednull
å’Œundefined
4. Use type inference to define component state or DefaultProps
Look at the following code:
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}
// ...
}
Copy the code
While the above code works, it can be improved by enabling TypeScript’s type system to correctly infer readonly types, such as 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}
// ...
}
Copy the code
In the above code, the TypeScript type system can now infer that DefaultProps and initialState are readonly types by freezing them.
In addition, by marking static defaultProps and state as readonly in the class, we eliminate the possibility of setting state to cause runtime errors mentioned above.
5. Declare Props/State using a type alias instead of an interface
While you can use an interface, it’s best to use Type for consistency and clarity, because there are cases where an interface doesn’t work. For example, in the previous example, we refactored the code so that TypeScript’s type system can correctly infer readOnly types by defining state types from the implementation. We can’t use the interface of this schema like the following code:
// works
type State = typeof initialState;
type Props = { someProps: string } & typeof defaultProps;
// throws error
interface State = typeof initialState;
interface Props = { someProps: string } & typeof defaultProps;
Copy the code
In addition, we cannot extend interfaces with types created by unions and intersections, so we must use type in these cases.
6. Do not use method declarations in interface/type
This ensures schema consistency in our code, since all members inferred by type/interface are declared the same way. Also, –strictFunctionTypes only work 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
}
Copy the code
Do not use FunctionComponent
Or FC for short to define a function component.
When using Typescript and React, function components can be written in one of two ways:
- As a normal function, the following code:
type Props = { message: string };
const Greeting = ({ message }: Props) = > <div>{message}</div>;
Copy the code
- Use React.FC or React.FunctionComponent as follows:
import React, {FC} from "react";
type Props = { message: string };
const Greeting: FC<Props> = (props) = > <div>{props}</div>;
Copy the code
Using FC provides some advantages, such as type checking and auto-completion for static properties such as displayName, propTypes, and defaultProps. But it has one known problem, which is breaking defaultProps and other properties: propTypes, contextTypes, displayName.
FC also provides an implicitly typed children property, which also has known problems. Also, as discussed earlier, component apis are supposed to be explicit, so an implicitly typed children attribute is not the best.
8. Do not use constructors on class components
With the new class attribute proposal, you no longer need to use constructors in JavaScript classes. Using constructors involves calling super () and passing props, which introduces unnecessary boilerplate and complexity.
We could write a cleaner, more maintainable React Class component 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}}Copy the code
In the code above, we see that there is less boilerplate involved in using class attributes, so we don’t have to deal with the this variable.
9. Do not 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}}Copy the code
Because all members of a class are public by default and at run time, there is no need to add additional 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}}Copy the code
10. Do not use private in component classes
Consider the following code:
import {Component} from "react"
class Friends extends Component {
private fetchProfileByID () {}
render () {
return // jsx blob}}Copy the code
In the above code, private privates the fetchProfileByID method only at compile time, because it is just a Typescript emulation. However, at run time, the fetchProfileByID method is still public.
There are different ways to privatize attributes/methods of JavaScript classes, using the underscore (_) variable naming principle as follows:
import {Component} from "react"
class Friends extends Component {
_fetchProfileByID () {}
render () {
return // jsx blob}}Copy the code
While this doesn’t really make the fetchProfileByID method private, it does a good job of communicating our intention to other developers that the specified method should be treated as private. Other techniques include using WeakMap, Symbol, and scoped variables.
But with the new ECMAScript class field proposal, we can do this easily and elegantly by using private fields, as shown below:
import {Component} from "react"
class Friends extends Component {
#fetchProfileByID () {}
render () {
return // jsx blob}}Copy the code
Also, TypeScript supports new JavaScript syntax for private fields in version 3.8 and above.
Additional: Do not use enum
Although enum is a reserved word in JavaScript, using enum is not a standard idiomatic JavaScript pattern.
But if you’re using a language like C # or JAVA, it can be tempting to use enUms. However, there are better patterns, such as using compiled type literals, as follows:
// 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.
}
Copy the code
conclusion
There’s no doubt that 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 process. It will help your engineering team gain visibility into technical debt, collaborate on code base issues, and better plan sprints.
Dev. to/alexomeyer/…