This article introduces some best practices for using TypeScript in React projects.

I. Project initialization

1. Build projects

The quickest way to initialize a React with TypeScript application is to use facebook’s official scaffolding create-React -app, which provides TypeScript templates. Run the following commands:

$ npx create-react-app my-app --template typescript
Copy the code

The generated project file is a little different, with the following configuration added:

  • .tsxUse:TypeScriptJSXFile extension;
  • tsconfig.json: Generated with default configurationTypeScriptConfiguration file;
  • react-app-env.d.ts:TypeScriptThe contents of the comment are used as compiler instructions.

2. VSCode extension and Settings

Install plug-in:

  • ESLint
  • Prettier - Code formatter

In the VSCode configuration file (.vscode/settings.json, the shortcut Command is Command + Shift + P/Ctrl + Shift + P) add the following configuration:

{
    "eslint.options": {
        "configFile": ".eslintrc.js"
    },
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    },
    "files.autoSave": "onFocusChange"."editor.formatOnType": true."editor.formatOnSave": true."eslint.alwaysShowStatus": true
}
Copy the code

The purpose of adding these configurations is to make VSCode better display ESLint errors/warnings, making it easier to find code errors, format code, and so on.

3. Configure ESLint/Prettier

  • addESlintRelated dependencies:
$ yarn add @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react --dev
Copy the code

The dependency to install ESLint is not selected here because the create-react-app scaffolding, the react-scripts package, already relies on ESLint and may cause version inconsistencies if installed repeatedly.

  • Create one in the root directory.eslintrc.jsFile and add the following:
module.exports = {
  parser: '@typescript-eslint/parser'.// Specify the ESLint parser
  extends: [
    'plugin:react/recommended'.// Use the recommended rules from @eslint-plugin-react
    'plugin:@typescript-eslint/recommended'.// Use the recommended rules from @typescript-eslint/eslint-plugin].parserOptions: {
    ecmaVersion: 2018.// Allows parsing of the latest ECMAScript features
    sourceType: 'module'.// Import is allowed
    ecmaFeatures: {
      jsx: true.// Allow parsing of JSX}},rules: {
    // Custom rules
    // e.g. "@typescript-eslint/explicit-function-return-type": "off",
    'linebreak-style': 'off'.'prettier/prettier': [
      'error',
      {
        endOfLine: 'auto',}]},settings: {
    react: {
      version: 'detect'.// tell eslint-plugin-react to automatically detect the react version,}}};Copy the code
  • addPrettierRely on:
$ yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
Copy the code
  • Create one in the root directory.prettierrc.jsFile and add the following:
module.exports = {
  semi: true.trailingComma: 'all'.singleQuote: true.printWidth: 120.tabWidth: 2};Copy the code
  • update.eslintrc.jsFile:
module.exports = {
  parser: '@typescript-eslint/parser'.// Specify the ESLint parser
  extends: [
    'plugin:react/recommended'.// Use the recommended rules from @eslint-plugin-react
    'plugin:@typescript-eslint/recommended'.// Use the recommended rules from @typescript-eslint/eslint-plugin
    'prettier/@typescript-eslint'.// Use eslint-config-prettier to disable the ESLint rule from @typescript-eslint where prettier conflicts with ESLint
    'plugin:prettier/recommended',].parserOptions: {
    ecmaVersion: 2018.// Allows parsing of the latest ECMAScript features
    sourceType: 'module'.// Import is allowed
    ecmaFeatures: {
      jsx: true.// Allow parsing of JSX}},rules: {
    // Custom rules
    // e.g. "@typescript-eslint/explicit-function-return-type": "off",
    'linebreak-style': 'off'.'prettier/prettier': [
      'error',
      {
        endOfLine: 'auto',}]},settings: {
    react: {
      version: 'detect'.// tell eslint-plugin-react to automatically detect the react version,}}};Copy the code
  • increaselintformatCommand:
"scripts": {
  "format": "prettier --write src/**/*.ts{,x}"."lint": "tsc --noEmit && eslint src/**/*.ts{,x}"
}
Copy the code

It is recommended that lint checks be added to commit hooks to avoid bad code or non-conforming code commits and pushes.

Second, best practices

1. Writing of components

Functional components are usually written in one of two ways:

import React from 'react';

// function declaration
function Heading() :React.ReactNode {
  return <h1>My Website Heading</h1>;
}

// Function extension
const OtherHeading: React.FC = () = > <h1>My Website Heading</h1>;
Copy the code
  • The first is written declaratively, stating that the return value of the function isReact.ReactNodeType;
  • The second way to write: use the function expression writing method, return afunctionNot a value or expression, so specify that the return value of this function isReact.FCType.

While developing, you can write either way, but the development team needs to be consistent and stick to one.

2, Props,

In TypeScript, we can use the interface or type keyword to declare props as generic.

type RowProps<Type> = {
  input: Type | Type[],
  onClick: () = > void, color? : string, };const Row: React.FC<RowProps<number | string>> = ({
  input,
  onClick,
  color = 'blue',}) = > {
  if (Array.isArray(input)) {
    return (
      <div>
        {input.map((i, idx) => (
          <div key={idx} onClick={onClick}>
            {i}
          </div>
        ))}
      </div>
    );
  }
  return <div style={{ color}} >{input}</div>;
};

const Rows = () = > {
  return (
    <Fragment>
      <Row input={[1]} onClick={()= >{}} / ><Row input={1} onClick={()= > {}} color={'red'} />
      <Row input={1} onClick={()= >{}} / ><Row input='1' onClick={()= >{}} / ><Row input={['1']} onClick={()= >{}} / ></Fragment>
  );
};
Copy the code

Props can be divided into several subsets, which can be classified into the following four categories based on actual application scenarios:

  • OwnProps, properties passed directly to the current component;

  • StateProps, properties obtained by connecting to store in redux;

  • DispatchProps, which is also a property obtained via connect to Redux;

  • RouteProps, a property passed through the route route in the react-router-dom

Multiple props can be connected using an ampersand:

type Props = OwnProps & RouteProps & StateProps & DispatchProps;
Copy the code

As for how to choose interface and type, my personal opinion is that both are ok, but the team members need to maintain a unified style. If you use interface, you can use the extends keyword to connect multiple props:

interface Props extends OwnProps, RouteProps, StateProps, DispatchProps {}
Copy the code

Here are two limitationspropsTwo common methods of property.

  • usePartialAll of thepropsProperties become optional

If all properties of props are optional, we can do this with Partial.

import React from 'react';
import { MouseEvent } from 'react';

type IProps = {
  color: 'red' | 'blue' | 'yellow';
  text: string;
  onClick(event: MouseEvent<HTMLDivElement>): void;
};

const Com: React.FC<Partial<IProps>> = ({ onClick, text, color }) = > {
  return (
    <div onClick={onClick} style={{ color}} >
      {text}
    </div>
  );
};

const App: React.FC = () = > {
  return <Com text="click" color={'blue'} / >;
};

export default App;
Copy the code
  • useRequiredWill allpropsProperties are set to mandatory

If all properties of props were mandatory, we could implement this with Required:

import React from 'react';
import { MouseEvent } from 'react';

type IProps = {
  color: 'red' | 'blue' | 'yellow';
  text: string;
  onClick(event: MouseEvent<HTMLDivElement>): void;
};

const Com: React.FC<Required<IProps>> = ({ onClick, text, color }) = > {
  return (
    <div onClick={onClick} style={{ color}} >
      {text}
    </div>
  );
};

const App: React.FC = () = > {
  return <Com text="click" color={'blue'} / >;
};

export default App;
Copy the code

Set up thePropsThe default value of

There are two ways to set the default values for Props.

  • passprops, set the default value
/* eslint-disable react/prop-types */
import React from 'react'; type IProps = { color? :'red' | 'blue' | 'yellow';
  text: string;
};

// Set the default value when passing props
const Com: React.FC<IProps> = ({ text, color = 'red' }) = > {
  return <div style={{ color}} >{text}</div>;
};

const App: React.FC = () = > {
  return (
    <div>
      <Com text="click" />
      <Com text="click" color="blue" />
    </div>
  );
};

export default App;
Copy the code
  • throughdefaultPropsSetting defaults
import React from 'react'; type IProps = { color? :'red' | 'blue' | 'yellow'; text? : string; };const Com: React.FC<IProps> = ({ text, color }) = > {
  return <div style={{ color}} >{text}</div>;
};

// Set the default value using defaultProps
Com.defaultProps = {
  color: 'red'.text: 'hello'};const App: React.FC = () = > {
  return (
    <div>
      <Com text="hi" />
      <Com color="blue" />
    </div>
  );
};

export default App;
Copy the code

3. Use in Hooks

  • useState

With useState, TypeScript can automatically infer types:

const App: React.FC = () = > {
  const [value, setValue] = useState(' ');
  return (
    <button
      onClick={()= > {
        setValue(1); // Error: Argument of type 'number' is not assignable to parameter of type 'SetStateAction<string>'
      }}
    >
      click
    </button>
  );
};
Copy the code

Sometimes we need to initialize state with a null value, which can be passed using generics:

type User = {
  email: string; id: number; error? : string; };const App: React.FC = () = > {
  const [user, setUser] = useState<User | null> (null);
  return (
    <button
      onClick={()= > {
        setUser({
          email: '[email protected]',
          id: 1427,
        });
      }}
    >
      click
    </button>
  );
};
Copy the code
  • useEffect

There is no return value and no need to pass the type.

  • useLayoutEffect

There is no return value and no need to pass the type.

  • useReducer

The useReducer accepts two parameters, Reducer and initialState. The reducer type can be inferred only by type constraints on the input state and action of the Reducer function passed in to the useReducer. Associative types are useful when using useReducer or Redux. Look at the following example:

import React, { useReducer } from 'react';

const initialState = { count: 0 };

/ * * joint type, use | link * /
type ACTIONTYPE =
  | { type: 'increment'; payload: number }
  | { type: 'decrement'; payload: string }
  | { type: 'reset'; payload: number };

function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.payload };
    case 'decrement':
      return { count: state.count - Number(action.payload) };
    case 'reset':
      return { count: action.payload };
    default:
      throw new Error();
  }
}

const Counter: React.FC = () = > {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: 'decrement', payload: '5' })}>-</button>
      <button onClick={()= > dispatch({ type: 'increment', payload: 5 })}>+</button>
      <button onClick={()= > dispatch({ type: 'reset', payload: 0 })}>reset</button>
    </>
  );
};

export default Counter;
Copy the code

When ACTIONTYPE is set to the union type, a variable of the union type is assigned to infer a type according to the rules of type inference.

  • useRef

UseRef can infer the type when passing a non-empty initial value. It can define the type by passing the first generic parameter, constraining the type of ref.current.

import React, { useLayoutEffect } from 'react';

export const App: React.FC = () = > {
  const h1Ref = React.useRef<HTMLDivElement | null> (null);

  function changeInnerText(el: HTMLDivElement, value: string) {
    el.innerText = value;
  }

  useLayoutEffect(() = > {
    if (null! == h1Ref.current) { changeInnerText(h1Ref.current,'hello world'); }} []);return <h1 ref={h1Ref}>My title</h1>;
};

export default App;
Copy the code

More useRef examples:

// <div> reference type
const divRef = React.useRef<HTMLDivElement | null> (null);

// <button> reference type
const buttonRef = React.useRef<HTMLButtonElement | null> (null);

// <br /> reference type
const brRef = React.useRef<HTMLBRElement | null> (null);

// <a> reference type
const linkRef = React.useRef<HTMLLinkElement | null> (null);
Copy the code
  • useCallback

UseCallback does not need to pass a type. It can infer the type from the return value of the function, and TypeScript will report an error if the type is passed incorrectly. Note, however, that the input parameter to the function needs to define a type, otherwise it is inferred to be any.

const multiplier = 2;

const multiply = useCallback((value: number) = > value * multiplier, [multiplier]);
Copy the code
  • useMemo

UseMemo does not need to pass in the type and can infer the type from the return value of the function.

const memoizedValue = React.useMemo(() = > {
  computeExpensiveValue(a, b)
}, [a, b])
Copy the code

To set the type on useMemo, simply pass the return value data type that you want to record to <>.

  • useContext

Here’s an example:

import React, { useContext } from 'react';

type IArticle = {
  id: number;
  title: string;
};

type ArticleState = {
  articles: IArticle[];
  setArticles: React.Dispatch<React.SetStateAction<IArticle[]>>;
};

const ArticleContext = React.createContext<ArticleState>({ articles: [].setArticles: (a) :void= >{}});const ArticleProvider: React.FC<React.ReactNode> = ({ children }) = > {
  const [articles, setArticles] = React.useState<IArticle[] | []>([
    { id: 1.title: 'post 1' },
    { id: 2.title: 'post 2'},]);return <ArticleContext.Provider value={{ articles.setArticles}} >{children}</ArticleContext.Provider>;
};

const ShowArticles: React.FC = () = > {
  const { articles } = useContext<ArticleState>(ArticleContext);

  return (
    <div>
      {articles.map((article: IArticle) => (
        <p key={article.id}>{article.title}</p>
      ))}
    </div>
  );
};

const ChangeArticles: React.FC = () = > {
  const { setArticles } = useContext<ArticleState>(ArticleContext);

  return <button onClick={()= > setArticles([])}>Empty Acticles</button>;
};

export const Article: React.FC = () = > {
  return (
    <ArticleProvider>
      <h1>My title</h1>
      <ShowArticles />
      <ChangeArticles />
    </ArticleProvider>
  );
};

export default Article;
Copy the code

4. Event handling

One of the most common cases is the form’s onChange event:

import React from 'react';

const MyInput: React.FC = () = > {
  const [value, setValue] = React.useState(' ');

  // The event type is ChangeEvent.
  // We pass "HTMLInputElement" to the input
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) = > {
    setValue(e.target.value);
  };

  return <input value={value} onChange={onChange} id="input-example" />;
};

export default MyInput;
Copy the code

For a tag and button tag click events, you can also use the union type to duplicate event handling:

import React from 'react';

const MyButton: React.FC = () = > {
  const handleClick = (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) = > {
    console.log(event);
  };

  return (
    <div>
      <button onClick={handleClick}>click</button>
      <a href="" onClick={handleClick}>
        link
      </a>
    </div>
  );
};

export default MyButton;
Copy the code

Common Event Event object types are as follows:

  • ClipboardEvent<T = Element>Clipboard event object
  • DragEvent<T = Element>Drag and drop event objects
  • ChangeEvent<T = Element>Change event object
  • KeyboardEvent<T = Element>Keyboard event object
  • MouseEvent<T = Element>Mouse event object
  • TouchEvent<T = Element>Touch event object
  • WheelEvent<T = Element>Wheel event object
  • AnimationEvent<T = Element>Animate event object
  • TransitionEvent<T = Element>Transition event object

5. Add third-party dependency libraries

When we need to add third-party dependencies to our code base, the first thing we do is check to see if the library has a package with the TypeScript @types type definition, by running the following command:

# yarn
$ yarn add @types/<package-name>

# npm
$ npm install @types/<package-name>
Copy the code

For example, if we want to introduce JEST into our project, we can install jEST dependencies as follows:

#yarn
$ yarn add @types/jest

#npm
$ npm install @types/jest
Copy the code

What if I don’t find a package with TypeScript type definition @types, for example loadsh doesn’t have @types/loadsh on NPM?

Install the loadsh dependency using NPM or YARN:

#yarn
$ yarn add loadsh

#npm
$ npm install loadsh
Copy the code

Then find the react-app-env.d.ts file in the SRC folder of your project and add declare to the loadsh dependency, adding the following code:

declare module 'loadsh' {
  const classes: any;
  export default classes;
}
Copy the code

Then normal introduction can be:

import _ from 'loadsh';
Copy the code

Third, refer to the article

  • How React and TypeScript Work Together
  • Type-safe state modeling with TypeScript and React Hooks