【新手向】快速开始一个基础Typescript的React项目.md

写在前面的话

一直以来,无论开发还是学习使用的框架都是以 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 ,如有错误,还请不吝指正~

创建基于 TypescriptReact 项目

如何开始项目呢?不用迷茫官方已经提供了工具 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

温馨提示:建议阅读英文文档,中文文档更新太慢~

初始化项目

通过上面的命令,创建默认项目结构如下:

image-20200826220203104

把与本文不相关的内容删除,并创建TodoList文件,使用 Typescript 编写 React 文件后缀为 .tsx。如下:

image-20200826220632443

修改 index.tsx 和 TodoList.tsx 文件。TodoList.tsx 文件如下:

image-20200826221204559

在 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 类型有两个属性 inputValuelist。当 Store 改变时,会更新该方法。
  • mapDispatchToProps 参数中默认有一个 dispatch,返回的一个对象。对象中的每一个属性都是一个函数会被当做 Redux action creator,另外每个函数的返回值是一个新函数。

聪明的你,有没有发现上面的代码中的问题?

至此,TodoList 项目已经编写完成。不过还有个小问题需要解决一下。运行起来看看效果:

error

我们已经完成了 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类型。

参考

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

繁华落尽Owenlee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值