Integration of React and Typescript

Posted by csatucd on Sun, 18 Aug 2019 12:03:18 +0200

 0. Typescript

Typescript is becoming more and more important for the front end, and many projects in the front end are refactored with Typescript. This is mainly due to the better type support of Typescript, which can do some type inference well in the coding process (mainly because the editor will have code prompts, it is very comfortable). Moreover, the syntax of Typescript is more rigorous than that of javascript, and it has better ES6 support. These features make it more efficient to use ts encoding and avoid the pits of ambiguity in JavaScript as far as possible. Recently, I am also learning some knowledge about Typescript, but my internship company seems to have no intention to use typescript for the time being, but I have to think about it by myself, trying to integrate typescript with react, which took quite a long time. The key is that variables need to be constrained in typescript.

1. Typeescript version project initialization of react

The react project can be initialized by using the react scaffold. By default, the javascript version of the project can be installed. By adding the configuration parameters of the typescript to the scaffold command, the react project of the typescript version can be initialized.

create-react-app react-todo-ts --typescript

2. react-todo-ts

This time, through a simple Todo application, we have a simple understanding of the process of integrating typescript in React. The directory structure I use is relatively simple (ps: According to common sense, a simple Todo application is not necessarily so complex, nor need to use redux to increase the complexity of the project, but here is just a demonstration, and in redux you also need to use typescript type declaration, otherwise you may not be able to pass. Compilation)

The catalogue structure is as follows:

To make a brief explanation:

  • Main Storage components in components

  • store contains some redux-related code

  • types only store common type definitions and interfaces

  • index.tsx is the default entry file for the project

Description of package.json file:

Several of these declaration files are unnecessary: @types/antd, @types/redux-thunk are unnecessary, and their type declaration files already exist in the antd and redux-thunk libraries. (In addition, redux-thunk was intended to simulate asynchronous requests, but there was a slight problem in the integration process, so there was no integration of asynchronous action s in this version).

{
  "name": "react-todo-ts",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@types/antd": "^1.0.0",
    "@types/jest": "24.0.17",
    "@types/node": "12.7.2",
    "@types/react": "16.9.2",
    "@types/react-dom": "16.8.5",
    "@types/react-redux": "^7.1.2",
    "@types/redux-thunk": "^2.1.0",
    "antd": "^3.21.4",
    "babel-plugin-import": "^1.12.0",
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-redux": "^7.1.0",
    "react-scripts": "3.1.1",
    "redux": "^4.0.4",
    "redux-thunk": "^2.3.0",
    "typescript": "3.5.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
​

Component splitting instructions:

3. Integration of typescript and antd

Select the Header component code here for illustration.

  1. Changes in Component Classes

    First, the Component class is changed, and we can restrict the type of state and props of components in a generic way.

interface IHeaderProps {
  todoList:ITodo[];
  addTodoAction: typeof addTodoAction
}
​
interface IHeaderState {
  todoText:string;
}
​
class Header extends Component<IHeaderProps, IHeaderState> {
  state = {
    todoText: ''
  }
    ...
    ...
  render() {
    return (
      <Row>
        <Col span={16}>
          <Input placeholder="please input todo: " value={this.state.todoText} onChange={(e) => this.handleChange(e)} onKeyDown={(e) => this.handleKeyDown(e)}></Input>
        </Col>
        <Col span={8}>
          <Button disabled={this.state.todoText.trim() === ''} type={'primary'} style={{ marginLeft: '50%', transform: 'translateX(-50%)' }} onClick={() => this.handleAdd()}>Add to</Button>
        </Col>
      </Row>
    )
  }
}

Component < IHeaderProps, IHeaderState > constrains the props and state attributes in the Header component. After doing so, the props attributes in the Header must satisfy the IHeaderProps interface, and the state must satisfy the IHeaderState interface.

  1. Changes in the code of the event interaction section

    handleChange = (e:ChangeEvent<HTMLInputElement>) => {
        const { value } = e.currentTarget;
        this.setState({ todoText: value });
      }
    ​
      handleAdd = () => {
        const { todoText } = this.state;
        if(todoText.trim() === '') {
          return;
        }
        this.props.addTodoAction({
          content: todoText,
          done: false
        });
        this.setState({ todoText: '' })
      }
    ​
      handleKeyDown = (e:KeyboardEvent<HTMLInputElement>) => {
        if(e.keyCode === 13) {
          console.log(e.keyCode);
          this.handleAdd();
        }
      }
      
       render() {
        return (
          <Row>
            <Col span={16}>
              <Input placeholder="please input todo: " value={this.state.todoText} onChange={(e) => this.handleChange(e)} onKeyDown={(e) => this.handleKeyDown(e)}></Input>
            </Col>
            <Col span={8}>
              <Button disabled={this.state.todoText.trim() === ''} type={'primary'} style={{ marginLeft: '50%', transform: 'translateX(-50%)' }} onClick={() => this.handleAdd()}>Add to</Button>
            </Col>
          </Row>
        )
      }

    When we define a function in ts, we have to define the type of function parameters. When we define handler function, we need to use event object, how can we declare the type of event object?

    At the beginning, I usually avoided reporting errors, whether 3721 is an any statement, but in fact it loses the meaning of type inference.

    In this project, react event types fall into two categories:

    1. Event types on antd components

      Event types in antd components are generally defined in antd libraries, but some components are identical to the original event definition types

    2. Event types on native components

      Event types of native components are generally defined in the @types/react library. Event types can be introduced from the react library. The naming of native event types is generally declared by means of (event name < element type >).

Under vscode, when you are uncertain about the event type, there will be a function signature prompt on the hover, so it is more convenient to determine the event type.


4. Integration of typescript and redux

Mainly for the operation of todoList

  1. Define an interface for the structure of todo

    export interface ITodo {
      content:String;
      done:boolean;
    }
  2. Determine the operation on todoList (add todo, delete todo, modify completion status), and then define the relevant action

    import { ADD_TODO, DELETE_TODO, CHANGE_TODO_STATUS } from './action-types';
    ​
    import { ITodo } from '../types';
    ​
    export const addTodoAction = (todo:ITodo):AddTodoAction => ({ type: ADD_TODO, todo });
    export const deleteTodoAction = (index:number):DeleteTodoAction => ({ type: DELETE_TODO, index });
    export const changeTodoStatusAction = (index:number):ChangeTodoStatusAction => ({ type: CHANGE_TODO_STATUS, index });
    ​
    ​
    export type AddTodoAction = {
      type: typeof ADD_TODO,
      todo: ITodo;
    }
    ​
    export type DeleteTodoAction = {
      type: typeof DELETE_TODO,
      index:number;
    }
    ​
    export type ChangeTodoStatusAction = {
      type: typeof CHANGE_TODO_STATUS,
      index:number;
    }
  1. Defining todoReducer, there are three possibilities for passing in the action of todoReducer, importing the type of action from actions.ts

    import { ADD_TODO, DELETE_TODO, CHANGE_TODO_STATUS } from './action-types';
    import { ITodo  } from '../types';
    import { AddTodoAction, DeleteTodoAction, ChangeTodoStatusAction } from './actions'
    ​
    const initTodoList:ITodo[] = [];
    ​
    export const todoReducer = (todos:ITodo[] = initTodoList, action:AddTodoAction | DeleteTodoAction | ChangeTodoStatusAction) => {
      switch(action.type) {
        case ADD_TODO:
          // Since there are three possible types of action input, it is not possible to accurately determine the action type. But after case judgment, the type of action should be determined, so here I use type assertion to assert action as AddTodoAction (the same below).
          return [(action as AddTodoAction).todo, ...todos];
        case DELETE_TODO:
          return todos.filter((todo, index) => index !== (action as DeleteTodoAction).index);
        case CHANGE_TODO_STATUS:
          const nextTodo:ITodo[] = [...todos];
          let target:ITodo = nextTodo.find((todo, index) => index === (action as ChangeTodoStatusAction).index) as ITodo;
          target.done = !target.done;
          return nextTodo;
        default:
          return todos;
      }
    }
    
  2. Store factory functions are exposed in the store, and store types can be retrieved through ReturnType.

    import { todoReducer } from './reducers';
    import { combineReducers, createStore, applyMiddleware} from 'redux';
    import thunk from 'redux-thunk';
    ​
    const rootReducer = combineReducers({
      todoList: todoReducer
    })
    ​
    export type RootState = ReturnType<typeof rootReducer>
    // Exposure of store Factory
    export function configStore() {
      return createStore(
        rootReducer,
        applyMiddleware(thunk)
      );
    }

5. react-redux integration

There is not much difference between react-redux and javascript versions in the way dependencies are separated|

  1. Packaging App Components with provider High-Level Components

    import React from 'react';
    import ReactDom from 'react-dom';
    import 'antd/dist/antd.css'
    ​
    import { Provider } from 'react-redux';
    import App from './components/app';
    import { configStore } from './store';
    ​
    const store = configStore();
    ​
    const Root = () => {
      return (
        <Provider store={store}>
          <App/>
        </Provider>
      )
    }
    ​
    ReactDom.render(
      (
        <Root/>
      ),
      document.querySelector('#root')
    );
    ​
  2. The main difference in introducing internal components is that the types of RootState need to be introduced together, and the types of parameters need to be defined when defining the mapStateToProps function.

    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { Row, Col, Checkbox, Button, Empty, message } from 'antd';
    ​
    import { RootState } from '../../store';
    import { ITodo } from '../../types';
    import { deleteTodoAction, changeTodoStatusAction } from '../../store/actions';
    ​
    interface IListProp {
      todoList:ITodo[];
      deleteTodoAction: typeof deleteTodoAction;
      changeTodoStatusAction:typeof changeTodoStatusAction;
    }
    ​
    class List extends Component<IListProp> {
    ​
      handleChange = (index:number) =>  {
        this.props.changeTodoStatusAction(index);
      }
    ​
      handleDelete = async (index:number) => {
        await this.props.deleteTodoAction(index);
        message.success("Successful deletion", 0.5);
      }
    ​
      render() {
        const { todoList } = this.props;
        return (
          <div>
          {
            todoList.length ? (
              <div>
                {
                  todoList.map((todo, index) => (
                   <Row key={index}>
                     <label>
                        <Col span={1}>
                          <Checkbox checked={todo.done} onChange={() => { this.handleChange(index) }}></Checkbox>
                        </Col>
                        <Col span={20}>
                          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
                            {
                              todo.content
                            }
                          </span>
                        </Col>
                        <Col span={3} style={{marginTop: '10px'}}>
                          <Button type={'danger'} size={'small'} onClick={() => {this.handleDelete(index)}}>delete</Button>
                        </Col>
                     </label>
                   </Row>
                  ))
                }
              </div>
            )
            :
            (<Empty/>)
          }
          </div>
        )
      }
    }
    ​
    const mapStateToProps = (state:RootState) => ({
      todoList: state.todoList,
    })
    ​
    export default connect(
      mapStateToProps,
      {
        deleteTodoAction,
        changeTodoStatusAction
      }
    )(List);
    ​

6. Asynchronous action

redux itself does not support asynchronous action, but it often needs to send asynchronous requests when it is used. In the process of integration, there are some problems. After the View layer sends an asynchronous action through the event, how to live the corresponding promise state, and then respond accordingly according to the promise state, may also need to see.

---------------------------------------------------------------------------------------

Project source code stamp - > https://github.com/zhangzhengsmiling/React-Todo-typescript.git

---------------------------------------------------------------------------------------

Topics: Javascript React TypeScript encoding