TypeScript如何应用在React中

1,247 阅读8分钟

介绍

前言

此篇文章的侧重点是TS如何在React中的使用,不在于TS的语法的梳理。

安装包

由于目前很多的JS库并没有自己的TypeScript的声明文件,所以TS官方自己提供了相关库的声明文件,我们需要下载如下两个包

npm i @type/react -s

npm i @types/react-dom -s

这里 @types 实际就是社区中的 DefinitelyTyped 库,定义了目前市面上绝大多数的 JavaScript 库的声明

所以下载相关的 JavaScript 对应的 @types 声明时,就能够使用使用该库对应的类型定义

使用

React事件类型

React事件其实是通过事件委托来优化内存,减少DOM事件绑定, ChangeEvent,MouseEvent,TouchEvent 其实是一个泛型类型,泛型变量为需要触发的事件提供类型

获取泛型类型的技巧

在行内写完代码,然后鼠标移动到e上面可以看到具体的事件对象类型

// input输入框输入文字
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e);
}

// button按钮点击
const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e);
}

// 移动端触摸div
const handleDivTouch = (e: React.TouchEvent<HTMLDivElement>) => {
  console.log(e);
}

常用 Event 事件对象类型:

  • ClipboardEvent<T = Element> 剪贴板事件对象
  • DragEvent<T = Element> 拖拽事件对象
  • ChangeEvent<T = Element> Change 事件对象
  • KeyboardEvent<T = Element> 键盘事件对象
  • MouseEvent<T = Element> 鼠标事件对象
  • TouchEvent<T = Element> 触摸事件对象
  • WheelEvent<T = Element> 滚轮事件对象
  • AnimationEvent<T = Element> 动画事件对象
  • TransitionEvent<T = Element> 过渡事件对象

T 接收一个 DOM 元素类型

从组件出发

无状态组件

首先我们需要了解React.FC(函数组件),其实React.FC是在TypeScript使用的一个泛型。

使用React.FC来写React组件时,不能用setState,需要配合Hooks来使用,React.FC 包含了 PropsWithChildren 的泛型,不用显式的声明 props.children 的类型。React.FC<> 对于返回类型是显式的,而普通函数版本是隐式的(否则需要附加注释)。

JS:

import React from "React";

export const Login = (props) => {
  const { images, name, alt } = props;

  return <img src={images} className={name} alt={alt} />;
}

以上代码是以JavaScript实现的,如果放到TS代码中则会报错,原因是函数组件接收到形参没有定义具体类型(props),所以在TS我们可以使用接口给props定义类型:

interface IProps {
  images?: string;
  name?: string;
  alt?: string;
  children?: ReactNode;
}

export const Login: React.FC<IProps> = (props) => {
  const { images, name, alt } = props;

  return <img src={images} className={name} alt={alt} />;
}

以上代码是一个比较规范的写法,React里定义了FC属性已经定义好了children类型,

  • React.FC 显式地定义了返回类型,其他方式是隐式推导的
  • React.FC 对静态属性:displayNamepropTypesdefaultProps 提供了类型检查和自动补全
  • React.FC 为 children 提供了隐式的类型(ReactElement | null)

有状态组件(类组件)

import React from 'react';

interface IProps {
    msg1?:any
}
interface IState {
    msg2:any
}

class Login extends React.Component<IProps,IState> {
    //构造函数
    constructor(props: IProps, context: any) {
        super(props, context);
        this.state={
            msg2:"test"
        }
    }
    render() {
        return (
            <div>
                <div>{this.state.msg2}</div>
                <div>{this.props.msg1}</div>
            </div>
        );
    }
}
export default Login

有关Component 泛型类的定义可以查看官方类型定义文件:

class Component<P, S> {
  readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;

  state: Readonly<S>;
}

受控组件

其实就是元素内容通过组件的状态进行控制。

例子: 一个input组件修改内部状态

 <input type='text'  onChange={change}  />
 
 
  const change=(e:React.KeyboardEvent<HTMLInputElement>)=>{
      this.setState({ text: e.target.value })
 }

类组件和函数组件的区别

数组件是一个纯函数,它接收一个props对象返回一个react元素。而类组件需要去继承React.Component并且创建render函数返回react元素,这将会要更多的代码,虽然它们实现的效果相同。 类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。

函数组件不能访问this对象,无法访问生命周期的方法,没有状态state

类组件有this,有生命周期,有状态state

函数组件只能访问输入的props,同样的props会得到同样的渲染结果,不会有副作用。

函数组件的性能比类组件的性能要高,不知道用什么组件类型时,推荐用React.FC

与hooks结合使用

useState

首先分两种情况:

1.如果在初始值就明确了类型时,就不用特意给useState指定特定泛型变量:

const [count, setCount] = useState(0);

//没有必要
const [count, setCount] = useState<number>(0);

2.当初始值是null或者undefined时,我们需要利用泛型确定想要的类型

interface IUser { 
   name: string;
   age: number;
} 
const [user, setUser] = React.useState<IUser | null>(null); 

console.log(user?.name);

有关useState类型定义官方源码:

function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

useRef

从作用来介绍:

1.可以用来存储变量,存储在函数式组件的外部,比起 useState,它不会存在异步更新的问题,也不会存在由capture-value特性引发的过时变量的问题,但是要注意赋值后由于ref引用没变,不会引起重渲染。

// 通过初始值来自动指明泛型变量类型
const num = useRef(0);

num.current = 24

console.log(num.current); // 24

这样就不会存在异步更新问题

2.可以用来连接DOM,获取DOm元素

// 连接DOM,初始值赋值为null,不能是undefined,如要指明泛型变量需要具体到HTMLxxxElement
// 如果我们确定iptRef.current在调用时一定是有值的,可以使用非空断言,在null后添加!
const iptRef = useRef<HTMLInputElement>(null!);

const handleClick = () => {
  iptRef.current.focus(); // 当然不用非空断言,
  iptRef.current?.focus()可选链也是可以的
}

return (
 <input type='text' ref={iptRef}  />
  <button onClick={handleClick}>点击</button>
)

有关useRef类型定义官方源码:

function useRef<T>(initialValue: T): MutableRefObject<T>;
    
interface MutableRefObject<T> {
    current: T;
}

结合redux使用

useSelector

useSelector用于获取store中的状态,第一个固定参数为函数,函数的入参即为store,而store的类型RootState需要在store中提前定义好

第一种方式:

const list = useSelector((state:{list:{id:number,name:string,isDone:boolean}[]}) => state.list)

第二种方式:

const list = useSelector<{list:[]},{id:number,name:string,isDone:boolean}[]>((state) => state.list)

以下是最简便的方式-优化 store.ts中 store.getState()可以获取到所有的模块

typeof 获取 某个数据的类型

ReturnType 获取函数类型的返回值的类型

const store = createStore(rootReducer);

export type RootState = ReturnType<typeof store.getState>

使用时:

const list = useSelector((state:RootState) => state.list)//从store中导入 批量获取模块的类型

useDispatch

useDispatch可以接收一个泛型参数用于指定Action的类型

import { Dispatch } from "react"
import { useSelector, useDispatch } from "react-redux"

import { RootStateType } from '../store'
export default function Home() {
    const dispatch = useDispatch<Dispatch<{type:string,payload:any}>>()
    //以上非必要
    const dispatch=useDispatch()
    dispatch({
        type: 'abc',
        payload: 123
    })

在action.ts中导出固定的actionType

export type ListAction =
   {
      type: 'ADD_list' // 字面量类型
      name: string
    }
  | {
      type: 'DEL_list'
      id: number
    }

export const addlist = (name: string): ListAction => {
  return {
    type: 'ADD_list',
    name
  }
}

export const dellist = (id: number): ListAction => {
  return {
    type: 'DEL_list',
    id
  }
}

在reducer中指定初始值的类型


type Listtype = {
    id:number,
    name:string,
    isDone:boolean
}

const initValue:Listtype[] =[]

redux thunk的使用

引入redux-thunk

import thunk from 'redux-thunk'
const store=createStore(reducer,composeWithDevTools(applyMiddleware(thunk)))

背景: thunk类型的变更,使用了thunk之后,返回的Action类型不再是对象,而是函数类型的Action,因此需要修改Action的类型。ThunkAction类型的使用

  • 类型参数1:ReturnType 用于指定函数的返回值类型 void
  • 类型参数2: 指定RootState的类型
  • 类型参数3: 指定额外的参数类型,一般为unkonwn或者any
  • 类型参数4: 用于指定dispatch的Action类型
  • 修改删除Action

提供一套开箱即用的代码 在store/index.ts中

import { applyMiddleware, createStore } from 'redux'
import reducer from './reducers'
import thunk, { ThunkAction } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
const store =  createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))

// store.getState()//可以获取到所有的模块
//typeof 获取 某个数据的类型   ReturnType 获取函数类型的返回值的类型
export type list = ReturnType<typeof store.getState>
// 所有的action中的类型
export type ActionType = 
  {type:'Update_state',payload:number} 
| {type:'ADD_state',payload:{name:string,isDone:boolean}}
| {type:'DELET_state',payload:number}
| {type:'GET_INFO',payload:[]}


//reducer中给所有初始值设置类型
export type TOdotype = {
    id:number,
    name:string,
    isDone:boolean
}

export type THunType=ThunkAction<void,list,unknown,ActionType>
export default store

注意:使用了ThunkAction后,因为目前版本原因,写代码不会给提示但是如果写错了代码会给相应的报错,如果需要提示的话可以根据需求下载低版本的包,

这里我提供一个小妙招:

在action/index.ts文件中

/// THunType从store导入的---type THunType=ThunkAction<void,list,unknown,ActionType>
export const getInfo=():THunType=>{
    return async(dispatch:Dispatch<ActionType>)=>{
    const res = await axios.get('xxxx')
    dispatch({
        type:'GET_INFO',
        payload:res.data.data
    })
    }
}

以上可见:在dispatch后面再套一层就可以完美解决这个问题

dispatch:Dispatch<ActionType>

React-router

安装

安装路由包

npm i react-router-dom@5.3.0

安装类型声明文件

npm i @types/react-router-dom

useHistory

useHistory可以用来做路由之间的跳转,并且在跳转时可以指定跳转参数state的类型

方法如下:

export function useHistory<HistoryLocationState = H.LocationState>(): H.History<HistoryLocationState>;

跳转功能

useHistory如果仅仅实现跳转功能,和js中使用语法一致

const history = useHistory()
const login = () => {
  history.push('/login') 
}

useHistory的进阶使用-页面跳转传参

跳转传参

在/login页面,通过state传递参数

const history = useHistory()
// 方式1
history.push('/home', { a:1 })
// 方式2
history.push({pathname: '/home', state:{a:1 }})

注意,如果希望跳转传参时,有指定的格式,可以给useHistory泛型提供类型参数,

例如:

const history = useHistory<{a: number }>()

获取参数

/home页面中,通过useLocation 来获取上一个页面传递的参数。

js的格式:

// 原来js的写法
import { useLocation } from 'react-router'
const location = useLocation()
const aa = location.state.a // 这里的a就是上一个页面中通过state传进来的

改进:

通过 useHistory可以通过泛型参数来指定state的类型

import { useLocation } from 'react-router'
export default function Home() {
    const location = useLocation<{ a: string }>()
    const a = location.state?.a

页面传参类型封装

type.d.ts

export type LoginState = { a: string } 

Login.tsx, Home.tsx

import { LoginState } from "../types"
export default function Home() {
   // 接收从login传入的值
   const location = useLocation<LoginState>()

细节处理

从login.tsx跳入home.tsx时会设置state,所以能获取具体的参数

直接访问home.tsx就会报错, 原因是没有传入state

改进:允许不传入state

// types.d.ts
export type LoginState = { a: string } | null

useParams

useParams接收一个泛型参数,用于指定params对象的类型

例子:

根组件

<BrowserRouter>
    <Link to="/page1/abc">page1</Link>
    <Route path="/page1/:id" component={Page1} />
</BrowserRouter>

page1.tsx

import { useParams } from 'react-router'
export default function Page1() {
    const params = useParams()
    console.log("params.id", params.id) //  这里会报错
}

解决方式

用useParams接收一个泛型参数

import { useParams } from 'react-router'

export default function Article() {
  const params = useParams<{ id: string }>()
  console.log(params.id)

  return (
    <div>
      文章详情
      <div>12</div>
    </div>
  )
}

小结:

用来接收参数时:useLocation, useParams要补充类型变量

用来传递参数的:useHistory可以不加