写在前面的话
一直以来,无论开发还是学习使用的框架都是以 Vue
为主,对 React
前端框架知之甚少。为了不掉队,也陆陆续续学了一些 React
知识,本文是入门项目笔记,使用 Typescript + React + Redux + React-redux
技术实现 TodoList
。本文从新手的角度分析如何编写基于 Typescript 的 React 项目,以及记录在编写过程中遇到的问题。
版本:
- Typescript@~3.7.2
- React@16.13.1
- react-redux@7.2.1
- redux@4.0.5
新手:React + Typescript ,如有错误,还请不吝指正~
创建基于 Typescript
的 React
项目
如何开始项目呢?不用迷茫官方已经提供了工具 create-react-app
。
创建普通 React
应用,只需要一行命令。
npx create-react-app todolist
创建基于 Typescript
的应用呢?只需在命令的后面添加--template
设置模板即可。
npx create-react-app todolist-ts --template typescript
执行完命令后,会创建项目并安装依赖。
PS: React 默认是使用 yarn
进行包管理,如果切换为 npm
可以在上面的命令后面添加 --use-npm
。
温馨提示:建议阅读英文文档,中文文档更新太慢~
初始化项目
通过上面的命令,创建默认项目结构如下:
把与本文不相关的内容删除,并创建TodoList文件,使用 Typescript 编写 React 文件后缀为 .tsx
。如下:
修改 index.tsx 和 TodoList.tsx 文件。TodoList.tsx 文件如下:
在 index.tsx 将 TodoList 引入即可,替换之前的 App
模块。
验证:终端执行
yarn run start
或者npm run start
命令,浏览器看到Hello world
即证明成功。
安装依赖库
项目基于 redux + react-redux 开发。安装依赖,并将 @types
声明文件一并安装。
yarn add redux react-redux @types/react-redux -S
@types/react-redux 是声明文件库,不安装在使用
react-redux
时编译器会提示。
TodoList小项目
接下来进入正文,先不着急写代码,分析下需要的功能。 TodoList
页面中一个输入框和提交按钮以及一个列表。在输入框中输入内容点击提交按钮后渲染出列表,点击列表中每一项时从列表中删除该项。
需要在 Store
中维护 inputValue
字符串和 list
数组,前者用存储输入框的值,后者用来记录列表中的内容。操作过程如下图:
需求分析完开始编写代码吧~
Store
首先我们需要定义 Store
。目录结构如下:
store
├── index.ts // store 入口文件
└── todo // todo 模块
├── actions.ts // actionCreators
├── reducer.ts // reducers
└── types.ts // 类型常量
store/index.ts
Store 的入口文件,在其中创建唯一的 store 对象。引入不同模块的 redecuer 。
import { createStore, combineReducers } from 'redux'
// 调试工具Redux-devtools
import { composeWithDevTools } from 'redux-devtools-extension'
// 引入 todo reducer
import todo from './todo/reducer'
// 由于 todo 是独立的模块,使用 combineReducers 赋值函数合并为一个
const reducers = combineReducers({
todo
})
// 创建 store 实例
const store = createStore(reducers, composeWithDevTools())
export default store
// 以 typeof reducer 的返回值构造一个类型
export type RootState = ReturnType<typeof reducers>
/***
* 相当于
* RootState = {
* todo: ITodoState
* }
* 类型,可以通过 todo 访问类型,后面会用到。
*/
todo/types
类型定义文件,定义一些常量和接口。
// state 类型
export interface ITodoState {
inputValue: string,
list: string[]
}
// 常量
export const ADD_ITEM = 'add_item'
export type ADD_ITEM = typeof ADD_ITEM
export const DELETE_ITEM = 'delete_item'
export type DELETE_ITEM = typeof DELETE_ITEM
export const CHANGE_INPUTVALUE = 'change_inputvalue'
export type CHANGE_INPUTVALUE = typeof CHANGE_INPUTVALUE
// action : 定义三个接口,分别对应:修改,新增、删除
export interface IAddItemAction {
type: ADD_ITEM
}
export interface IDeleteItemAction {
type: DELETE_ITEM,
index: number
}
export interface IChangeInputValueAction {
type: CHANGE_INPUTVALUE,
text: string
}
// 为三个接口定义别名: 联合类型表明所有可能操作。
export type ITodoAction = IAddItemAction | IDeleteItemAction | IChangeInputValueAction
todo/actions
Action
: 是数据是应用传递到 Store 唯一途径。描述 Store 的改变事件,不具体参与改变过程。
import {
IAddItemAction,
ADD_ITEM,
IDeleteItemAction,
DELETE_ITEM,
IChangeInputValueAction,
CHANGE_INPUTVALUE,
} from './types'
// 定义三个 actionCreators
export const addItem = (): IAddItemAction => ({ type: ADD_ITEM })
export const delelteItem = (index: number): IDeleteItemAction => ({
type: DELETE_ITEM,
index,
})
export const changeInputValue = (text: string): IChangeInputValueAction => ({
type: CHANGE_INPUTVALUE,
text,
})
todo/reducer
Reducer
中定义数据如何响应 Actions 并发送给 Store。前面 Action 只是描述数据需要更新,而Reducer才是真正的处理响应发送给 Store。
import {
ITodoState,
ITodoAction,
ADD_ITEM,
DELETE_ITEM,
CHANGE_INPUTVALUE,
} from './types'
// 初始 state
const defaultState: ITodoState = {
inputValue: '',
list: [],
}
// reducers
export default (state = defaultState, actions: ITodoAction): ITodoState => {
const newState = { ...state }
switch (actions.type) {
case CHANGE_INPUTVALUE: // 修改
newState.inputValue = actions.text
return newState
case ADD_ITEM: // 增加
const text = newState.inputValue
newState.list.push(text)
newState.inputValue = ''
return newState
case DELETE_ITEM: // 删除
const index = actions.index
newState.list.splice(index, 1)
return newState
default:
return newState
}
}
React-redux和页面
Store搭建完成后,使用
react-redux使用它。这里是有到
react-redux` 提供的两个核心 API :
<Provider store>
:<Provider store>
使组件层级中的connect()
方法都能够获得 Redux store。通常将根组件嵌套其中。connect()
: 连接 React 组件与 Redux store。
具体可以参考文档:React-redux Api - 中文
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';
// 引入 Provider 将 store 绑定到页面中
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}> {/* 绑定 */}
<TodoList />
</Provider>,
document.getElementById('root'))
src/TodoList.tsx
import React from 'react'
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { IRootState } from './store'
import * as actions from './store/todo/actions'
const TodoList: React.FC<TodoListProps> = (props) => {
const { inputValue, list, addItem, deleteItem, setInputValue } = props
return (
<>
<div>
<input value={inputValue} onChange={setInputValue} />
<button onClick={addItem}>提交</button>
</div>
<ul>{list.map((item, index) => {
return (<li key={index} onClick={() => {deleteItem(index)}}>{item}</li>)
})}
</ul>
</>
)
}
interface ITodoStateProps {
inputValue: string
list: string[]
}
interface ITodoDispatchProps {
addItem: () => void
deleteItem: (index: number) => void
setInputValue: (e: React.ChangeEvent<HTMLInputElement>) => void
}
type TodoListProps = ITodoStateProps & ITodoDispatchProps
// 以 mapStateToProps 和 mapDispatchToProps 类型为 参数
// type TodoListProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>
// // 监听 Store 的变化,只要 Rect store 发生改变,该方法会被调用。
const mapStateToProps = (state: IRootState): ITodoStateProps => {
const { todo } = state
return {
inputValue: todo.inputValue,
list: todo.list,
}
}
// 定义操作方法
const mapDispatchToProps = (dispatch: Dispatch): ITodoDispatchProps => {
return {
addItem: () => dispatch(actions.addItem()),
deleteItem: (index) => dispatch(actions.delelteItem(index)),
setInputValue: (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value
dispatch(actions.changeInputValue(text))
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoList)
mapStateToProps
返回值是ITodoStateProps
类型有两个属性inputValue
和list
。当 Store 改变时,会更新该方法。mapDispatchToProps
参数中默认有一个dispatch
,返回的一个对象。对象中的每一个属性都是一个函数会被当做Redux action creator
,另外每个函数的返回值是一个新函数。
聪明的你,有没有发现上面的代码中的问题?
至此,TodoList 项目已经编写完成。不过还有个小问题需要解决一下。运行起来看看效果:
我们已经完成了 TodoList 部分功能的的实现,输入、添加可以正常运行,但是当点击删除的时候store 中的数据被删除了,而页面并没有重新渲染。问题出在哪里呢?
分析定位问题:
inputValue
的值是可以改变,列表页是可以新增的那么证明Store
是正常的。- 删除的时触发
deleteItem
方法 list 发生了改变。那么删除逻辑在Store
中也是正确的。 - 通过打印日志,发现在删除的时候
mapStateToProps
是会被调用 list 也发生了改变,也就是说页面中监听到Store
的改变。 - 调试发现
TodoList
在 list 删除时,Props
中的list
并没有改变。定位到问题,就是在删除时,mapStateToProps
监听到list
的改变传给组件中,但是组件并没认为Props
中的list
发生了改变导致页面没有重新渲染。 - 为什么组件不认为
list
发生了改变呢?为什么同样的inputValue
就没有问题?这里就有提到类型,inputValue
是值类型,而list
作为数组时引用类型。组件对新的props
进行浅层的判等检查,所以导致无法检测到 list 的改变。
修改方法 mapStateToProps
返回 list 的副本。
const mapStateToProps = (state: IRootState): ITodoStateProps => {
const { todo } = state
return {
inputValue: todo.inputValue,
list: [...todo.list],
}
}
至此,TodoList 项目完成。(PS: 图片就不放了~)
知识点补充
表单元素 onChange 事件类型
- input, slider: React.ChangeEvent
- textarea: React.ChangeEvent
- select: React.ChangeEvent
指定函数组件 Props 类型的方法
使用 React.FC<> 指定 Props 的类型,例如:上面指定Props为ReduxType类型。