likes
comments
collection
share

用TS+reactHooks+redux完成一个todos

作者站长头像
站长
· 阅读数 55

整体目标

在react项目中使用TS,具体学习如何使用:

  • hooks:useState,useEffect,useRef等hooks的使用
  • redux:useDispatch, reducer, useSelector等的使用

完成一个TODOS

useEffect的使用

目标

掌握useEffect hook在typescript中的使用

useEffect的格式

useEffect用来管理副作用(例如 API 调用,事件添加....)

示例用法

useEffect(() => {
  setInterval(()=>{
    
  },1000)
}, [])

在typescript中使用和javascript中使用完全一致

学习useEffect的定义格式

/**
 * Accepts a function that contains imperative, possibly effectful code.
 *
 * @param effect Imperative function that can return a cleanup function
 * @param deps If present, effect will only activate if the values in the list change.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#useeffect
 */
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
  • useEffect函数不涉及到任何泛型参数,在typescript中使用和javascript中使用完全一致。
  • ? 表示可选
  • DependencyList是一个readonly any[]

小结

  1. useEffect的使用在ts和js中是一致的;
  2. 通过查看ts的类型声明文件(一定程度上可代替文档),就可以知道它的用法

useState的使用

目标

掌握useState 在typescript中的使用

useState的格式

导入useState

通过ctrl+点击,查看格式

useState的类型声明如下

/**
 * Returns a stateful value, and a function to update it.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#usestate
 */
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

标准用法-接收泛型参数

useState接收一个泛型参数,用于指定初始值的类型

// 使用泛型
const [name, setName] = useState<string>('张三')
const [age, setAge] = useState<number>(28)
const [isProgrammer, setIsProgrammer] = useState<boolean>(true)

// 如果你在set函数中的参数不符合声明的变量类型,程序会报错
<button onClick={() => setName(100)}>按钮</button>  // 报错

简便用法-使用类型推论省略泛型参数

在使用useState的时候,只要提供了初始值,typescript会自动根据初始值进行类型推断,因此useState的泛型参数可以省略

const [name, setName] = useState('张三')
const [age, setAge] = useState(28)
const [isProgrammer, setIsProgrammer] = useState(true)

小结

  1. useState本身是需要接收泛型参数的,由于有类型推断机制,这个泛型可以省略
  2. useState的使用与js基本一致

useState 进阶用法

目标

能够使用useEffect发送请求,用useState保存数据,并且进行渲染

背景

接口:GET, http://geek.itheima.net/v1_0/channels

需求:发送请求获取频道列表数据,并且渲染

初始写法

按js的方式写

import { useEffect, useState } from 'react'
import axios from 'axios'

export default function App() {
  // 存放频道列表数据
  const [list, setList] = useState([])
  useEffect(() => {
    const fetchData = async () => {
      const res = await axios.get('http://geek.itheima.net/v1_0/channels')
      setList(res.data.data.channels)
    }
    fetchData()
  }, [])
  return (
    <div>
      <ul>
        {list.map((item) => {
          return <li key={item.id}>{item.name}</li> {/*这里会报错:*/}
        })}
      </ul>
    </div>
  )
}

遇到错误

用TS+reactHooks+redux完成一个todos

解决

有如下三种方式

  1. 提前给初始值,便于类型推导

    const [list, setList] = useState([{id:1, name: 'test'}])

  2. 补充泛型参数:any大法

    const [list, setList] = useState<any[]>([])

  3. 补充泛型参数:按接口返回值自定义类型

    type Channel = { id: number, name: string }
    
    const [list, setList] = useState<Channel[]>([])
    

拓展: never类型

表示一个不能取到的类型。

一般看到它,就表示代码有问题,它到底有什么用处?拓展:www.zhihu.com/question/35…

useRef的使用

目标

能够使用useRef配合ts操作DOM

复习useRef

需求

<input type="text" />
<button>非受控组件获取input的值</button>

基本代码

const inputRef = useRef(null)
<input type="text" ref="inputRef" />
onClick = () => {
  console.log(inputRef.current.value)  // 这里报错
}

报错

用TS+reactHooks+redux完成一个todos

useRef的泛型参数

useRef 接收一个泛型参数,源码如下

/**
 * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
 * (`initialValue`). The returned object will persist for the full lifetime of the component.
 *
 * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
 * value around similar to how you’d use instance fields in classes.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#useref
 */
function useRef<T>(initialValue: T): MutableRefObject<T>;
    
interface MutableRefObject<T> {
    current: T;
}

useRef的泛型参数用于指定current属性的值的类型

解决问题

使用useRef操作DOM,需要明确指定所操作的DOM的具体的类型,否则current属性会是null

正确语法:

const inputRef = useRef<HTMLInputElement>(null)

练习

如何获取button按钮的应用?

拓展

  1. 为啥初始值一般是null?
  2. 如何获取一个DOM对象的类型?

null可以代表对象类型;

在jsx中,鼠标直接移动到该元素上,就会显示出来该元素的类型

可选链操作符

目标

掌握js中的提供的可选链操作符语法

可选链操作符(  ?.  )

可选链操作符(  ?.  )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。

它不是ts中独有的内容,而是js中的语法。

参考文档:developer.mozilla.org/zh-CN/docs/…

let nestedProp = obj.first?.second;
console.log(res.data?.data)
// -----
obj.fn?.()

if (obj.fn) {
    obj.fn()
}
obj.fn && obj.fn()

非空断言

目标

掌握ts中的非空断言的使用语法

作用

如果我们明确地知道对象的属性一定不会为空,那么可以使用非空断言 !

用TS+reactHooks+redux完成一个todos

格式

对象!.属性

// 告诉typescript, 明确的指定obj不可能为空
let nestedProp = obj!.second;

注意:非空断言一定要确保有该属性才能使用,不然使用非空断言会导致bug

todos案例介绍

目标:体会ts在react中的综合使用

  • 用redux-thunk请求回来数据
  • 把数据保存在redux中
  • 用useSelector取出数据

redux在ts中基本使用

目标

掌握在ts项目中如何初始化redux

安装依赖包

npm i redux react-redux redux-thunk redux-devtools-extension

目录结构

store的基本结构都是固定的套路,注意如下的store:

├── store
│   ├── index.ts      # 
│   └── reducers      # 
│       ├── index.ts  # 
│       └── todos.ts  # 
├── App.tsx           # 根组件
├── index.js          # 项目入口
└── package.json

创建文件

新建文件 store/index.ts

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

export default store

新建文件 store/reducers/index.ts

import { combineReducers } from 'redux'
import todos from './todos'
const rootReducer = combineReducers({
  todos,
})
export default rootReducer

新建文件 store/reducers/todos.ts

const initValue = [
  {
    id: 1,
    name: '吃饭',
    isDone: false,
  },
  {
    id: 2,
    name: '睡觉',
    isDone: true,
  },
  {
    id: 3,
    name: '打豆豆',
    isDone: false,
  },
]
export default function todos(state = initValue, action: any) {
  return state
}

index.tsx中

import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

测试

在react-redux调试工具中查看效果

小结

reducer中的action比较复杂,先给any

useSelector的使用

目标

掌握useSelector在ts中的使用

useSelector的基本使用

按之前的js的使用格式,它会报错

// 获取todos数据
const todos = useSelector( state => state.todos) // 这里会报错

要改造错误,先学下格式。

useSelector格式

useSelector接收两个泛型参数

  • 第一个泛型类型:TState的默认值是DefaultRootState(就是{})用于指定state的类型
  • 第二个泛型类型:TSelected用于指定返回值的类型
export function useSelector<TState = DefaultRootState, TSelected = unknown>(
    selector: (state: TState) => TSelected,
    equalityFn?: (left: TSelected, right: TSelected) => boolean
): TSelected;

使用

方式1: 指定泛型类型

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

用TS+reactHooks+redux完成一个todos

方式2:直接指定回调函数的形参类型

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

注意,我们没有指定返回值的类型。因为它可以自己推论出来

参考文档open in new window

渲染todos

<h1>todos</h1>
<ul>
    {
       todos.map(it =>(<li key={it.id}>{it.name}- {it.isDone}</li>))
    }
</ul>

获取RootState的类型-问题及思路

问题

随着reducer的模块越来越多,它的类型会越来越复杂

如何准确、方便地获取到store的类型?

思路

  1. 拿到全部的状态。在store的index.ts中 拿到全部的状态store.getState()
  2. 从这个全部的状态反向分析得到数据类型声明
  3. 导出这个类型声明,在useSelector中使用

typeof和ReturnType

typeof

typeof可以获取某个数据的类型

参考文档:react-redux.js.org/using-react…

function fn(n1: number, n2:number):number {
  return n1 + n2
}

// 获取fn函数的类型。这里鼠标悬浮提示就能看到
type Fn = typeof fn

ReturnType

ReturnType是一个泛型工具类型,可以获取函数类型的返回值的类型

格式

type 返回值的类型 = ReturnType<函数f的类型>

示例

function fn(n1: number, n2:number):number {
  return n1 + n2
}

// 获取fn函数的类型
type Fn = typeof fn

// 获取Fn函数的返回值类型
type Res = ReturnType<Fn> // 鼠标悬浮查看提示

获取RootState的类型-实操

定义RootState类型并导出

在store/index.ts中定义RootState类型并导出

store/index.ts

export type RootState = ReturnType<typeof store.getState>

在业务组件中导入使用

导入useSelector的正确用法

import { RootState } from '../store'

// 获取todos数据
const todos = useSelector((state: RootState) => state.todos)

切换状态分析

用TS+reactHooks+redux完成一个todos

useDispatch的使用

目标

掌握useDispatch在ts中的使用

基本格式

const dispatch = useDispatch()

与在js中的写法一致。

此时,dispatch函数可以使用传入任意的内容。

补充泛型

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}>>()
    dispatch({
        type: 'abc',
        payload: 123
    })

参考链接:react-redux.js.org/using-react…

实现已完成todos项-准备

准备Action

建立文件:'../store/actions/index'

export const delTodo = (id: number) => {
  return {
    type: 'DEL_TODO',
    id
  }
}

准备删除按钮

import { delTodo } from '../store/actions/index'
const dispatch = useDispatch()
    
const del = (id: number)=>{
  dispatch(delTodo(id))
}

return (
  <div>
  <ul>
    {
      todos.map(it => (<li className={it.isDone?'compeleted':''}key={it.id}>
        {it.title}
          <button onClick={()=>del(it.id)}>删除</button>
      </li>))
    }
  </ul>
</div>
)

准备删除样式

reducer的改造-提炼初始值的类型

目标

掌握reducers在TS中的写法

提炼todo的类型

type TodoType = {
    id: number
    title: string
    isDone: boolean
}
const initState: TodoType[] = []
// const initState = [{
//         id: 1,
//         title: '吃饭',
//         isDone: true
//     },
//     {
//         id: 2,
//         title: '学习',
//         isDone: false}
//     ]

export default function todos(state = initState, action: any): TodoType[]{
    console.log(action)
    return state
}

reducer的改造-给action提供类型

目标

在写reducer中的action时,提供类型

思路

在action.ts中导出固定的actionType

在reducer中导入actionType

在action.ts中导出固定的actionType

export type TodoAction =
   {
      type: 'ADD_TODO' // 字面量类型
      name: string
    }
  | {
      type: 'DEL_TODO'
      id: number
    }

export const addTodo = (name: string): TodoAction => {
  return {
    type: 'ADD_TODO',
    name
  }
}

export const delTodo = (id: number): TodoAction => {
  return {
    type: 'DEL_TODO',
    id
  }
}

在reducer中指定初始值的类型

type TodoType = {
    id: number
    title: string
    isDone: boolean
}
import { TodoAction } from '../actions/index'
const initState: TodoType[] = []
export default function todos(state = initState, action: TodoAction): TodoType[]{
    console.log(action)
    if(action.type === "ADD_TODO") {
        return state
    } else if(action.type === "DEL_TODO") {
        return state
    }
    return state
}

实现删除功能

const dispatch = useDispatch()
    
const del = (id: number)=>{
  dispatch(delTodo(id))
}

结构

<ul>
  {
    todos.map(it => (<li className={it.isDone?'compeleted':''}key={it.id}>
        {it.name}
        <button onClick={()=>del(it.id)}>删除</button>
      </li>))
  }

</ul>

完成添加功能

受控组件

const [name, setName] = useState('')
// const dispatch = useDispatch<Dispatch<{type:string,payload:any}>>()
const dispatch = useDispatch()

const del = (id: number)=>{
  dispatch(delTodo(id))
}
    
const change = (e:any) => {
  setName(e.target.value)
}
const onKeyUp = (e:any) => {
  if(e.keyCode === 13) {
    alert(name)
    dispatch(addTodo(name))
  }
}

结构

<input type="text" value={name} onChange={change}  onKeyUp={onKeyUp}/> 

事件对象的类型

目标

掌握事件对象在TS中如何指定类型

内容

在使用事件对象时,需要指定事件对象的类型

const add = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.code === 'Enter') {
    dispatch(addTodo(name))
    setName('')
  }
}
// 或者
const add = (e:React.KeyboardEvent<HTMLInputElement>) => {
    console.log(e)
    if(e.code === "Enter") {
      const t = e.target as HTMLInputElement
      dispatch(addTodo(t.value))
      // if('value' in e.target) {
      //   console.log(e.target.value )
      // }
    }
  }

技巧

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

用TS+reactHooks+redux完成一个todos

完成修改状态

目标

完成修改todo状态的功能

补充action

export type ActionType = {
    type: 'DEL_TODO',
    id: number
} | {
    type: 'ADD_TODO',
    name: string
} | {
    type: 'CHANGE_STATE_TODO',
    payload:{
        id:number,
        newIsDone: boolean
    }
}

export const changeStateTodo = (id:number, newIsDone:boolean): ActionType => {
    return {
        type: "CHANGE_STATE_TODO", 
        payload: {
            id,
            newIsDone
        }
    }
}

export const delTodo = (id:number): ActionType => {
    return {
        type: 'DEL_TODO', 
        id: id
    }
}


export const addTodo = (name:string): ActionType => {
    return {
        type: 'ADD_TODO', 
        name,
    }
}

修改reducer

import { ActionType } from '../actions/index'

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

const initState: TodoItem[] = [
]


export default function todos(state:TodoItem[] = initState, action: ActionType): TodoItem[] {
    console.log(action)
    // if(action.type === "DEL_TODO") {
    //     return state.filter(it=>it.id !== action.id)
    // }

    if(action.type === "DEL_TODO"){
        return state.filter(it=>it.id !== action.id)
    }else if(action.type === "ADD_TODO") {
        return [...state, {id: Date.now(), name: action.name, isDone:false}]
    }else if(action.type === "CHANGE_STATE_TODO"){
        // 修改状态 
        // action.payload.id
        // action.payload.newIsDone

        return state.map(it=>{
            if(it.id === action.payload.id) {
                return {...it, isDone: action.payload.newIsDone}
            } else {
                return {...it}
            }
        })
    }
    return state
}

redux thunk的使用

目标

掌握redux thunk在typescript中的使用

核心代码

引入redux-thunk

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

完成初始化功能

action中补充获取数据的action

export type TodoAction =
  | {
      type: 'ADD_TODO' // 字面量类型
      name: string
    }
  | {
      type: 'DEL_TODO'
      id: number
    }
    | {
      type: 'INIT_TODO'
      data: []
    }
export function getTodos() {
  return async (dispatch:any) => {
    const res = await axios.get('https://www.fastmock.site/mock/37d3b9f13a48d528a9339fbed1b81bd5/book/api/todos')

    // 异步操作
    dispatch({
      type: 'INIT_TODO',
      data: res.data.data
    })
  }
}

在Home.tsx中派发

 useEffect(() => {
        dispatch(getTodos())
      }, [dispatch])

ThunkAction类型的使用

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

参考文档:redux.js.org/usage/usage…

// 类型参数1:ReturnType 用于指定函数的返回值类型 void
// 类型参数2: 指定RootState的类型
// 类型参数3: 指定额外的参数类型,一般为unkonwn或者any
// 类型参数4: 用于指定dispatch的Action类型
// 修改删除Action
export function delTodo(id: number): ThunkAction<void, RootState, unknown, TodoAction> {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({
        type: 'DEL_TODO',
        id,
      })
    }, 1000)
  }
}

推荐做法

在store/index.ts中

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

export type RootStateType = ReturnType<typeof store.getState>
export type RootThunkAction = ThunkAction<void, RootStateType, unknown, TodoAction>


export type RootThunkAction = ThunkAction<void, RootState, unknown, TodoAction>

redux-thunk版本bug

redux-thunk@2.4.0新版中,使用dispatch的时候,会丢失提示,需要降级到2.3.0版本

参考:www.npmjs.com/package/red…

拓展:unknown类型

目标

了解什么是TS中的unknown类型

unknown

unknown是更加安全的any类型

  1. any 类型进行任何操作,不需要检查类型。
// 没有类型检查就没有意义了,跟写JS一样
// 不安全
let value:any
value = true
value = 1
value.length

unknown是更加安全的any类型

可以把任何值赋值给 unknown,但不能调用属性和方法; 除非使用类型断言或者类型收窄

let value:unknown// 定义为
value = 'abc'// 类型断言
console.log( (value as string).length )

// 类型收窄
if (typeof value === 'string') {
  value.length
}

小结

// any:    任意类型
// unknown: 更安全的any类型
// never:   不可能实现的类型
转载自:https://juejin.cn/post/7127602770810503175
评论
请登录