用TS+reactHooks+redux完成一个todos
整体目标
在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[]
小结
- useEffect的使用在ts和js中是一致的;
- 通过查看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)
小结
- useState本身是需要接收泛型参数的,由于有类型推断机制,这个泛型可以省略
- 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>
)
}
遇到错误
解决
有如下三种方式
-
提前给初始值,便于类型推导
const [list, setList] = useState([{id:1, name: 'test'}])
-
补充泛型参数:any大法
const [list, setList] = useState<any[]>([])
-
补充泛型参数:按接口返回值自定义类型
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) // 这里报错
}
报错
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按钮的应用?
拓展
- 为啥初始值一般是null?
- 如何获取一个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中的非空断言的使用语法
作用
如果我们明确地知道对象的属性一定不会为空,那么可以使用非空断言 !
格式
对象!.属性
// 告诉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)
方式2:直接指定回调函数的形参类型
const todos = useSelector((state: {todos:{id:number, name:string,isDone:boolean}[]}) => state.todos)
注意,我们没有指定返回值的类型。因为它可以自己推论出来
渲染todos
<h1>todos</h1>
<ul>
{
todos.map(it =>(<li key={it.id}>{it.name}- {it.isDone}</li>))
}
</ul>
获取RootState的类型-问题及思路
问题
随着reducer的模块越来越多,它的类型会越来越复杂
如何准确、方便地获取到store的类型?
思路
- 拿到全部的状态。在store的index.ts中 拿到全部的状态
store.getState()
- 从这个全部的状态反向分析得到数据类型声明
- 导出这个类型声明,在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)
切换状态分析
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上面可以看到具体的事件对象类型
完成修改状态
目标
完成修改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版本
- github.com/reduxjs/red…
npm i redux-thunk@2.3.0
拓展:unknown类型
目标
了解什么是TS中的unknown类型
unknown
unknown是更加安全的any类型
- 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