> For the complete documentation index, see [llms.txt](https://gb.akanote.cn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gb.akanote.cn/cms/part2-react+ts.md).

# Part2-React+TS

## 一、项目创建与准备

### 1、创建项目

创建名为 `hook-ts` 的项目：

```shell
$ npx create-react-app hook-ts --template typescript
$ cd hook-ts
# 通过vscode打开
$ code .
```

可以看到，整个项目的组件，都是使用tsx做后缀的，而普通的js文件，也使用的是ts作为后缀名。

> 注意：
>
> 1、在React中，以js为后缀名文件也可以作为组件使用，但在React+TS中，请慎用。最好区分开，ts代表js，tsx代表jsx。
>
> 2、js与jsx也可以在React+TS中使用，是兼容的，只是既然使用了ts，那就尽量使用ts和tsx代替它们。

### 2、项目初始化

先把 `src` 下所有的文件清空，并新建 `index.ts` 与 `App.tsx` 。

mac用户：

```shell
$ cd src
$ rm *
# 创建index.ts与App.tsx
$ touch index.ts App.tsx
# 返回根目录
$ cd ..
```

win用户手动删或执行 `del *` 可删除src下的文件，或者打开 `git bash` 也可操作以上命令。

### 3、App组件

任意给 `App.tsx` 写点内容：

```tsx
import React from 'react'

const App = () => {
    return (
        <h2>你好世界</h2>
    )
}

export default App;
```

### 4、项目入口文件

在 `index.ts` 中：

```ts
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
    <App />,
    document.getElementById("root")
)
```

但是你会发现一直报错：

[![image-20211214204437532](https://tva1.sinaimg.cn/large/008i3skNgy1gxdndhe3htj31te0q0af1.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdndhe3htj31te0q0af1.jpg)

原因是ts文件会被解析为js，而React+TS的项目是不允许ts成为一个组件的，因此只需要将 `index.ts` 改为 `index.tsx` 即可。

### 5、项目运行

最后，项目根目录下运行：

```shell
$ npm run start
```

即可将项目运行。

## 二、Function Component

### 1、tsconfig

项目中如果每次都使用相对路径引入组件，就比较麻烦，这里推荐配置 `tsconfig.json`：

```json
{
  "compilerOptions": {
    "target": "ESNext",	// 配置ES语法
    "baseUrl": "./src",		// 配置基础路径
    "jsx": "preserve"		// 在preserve模式下生成的代码中会保留JSX以供后续的转换操作使用（如babel）
  }
}
```

修改以上这几项，其中jsx的配置解释：

| 模式         | 输入        | 输出                           | 输出文件扩展名 |
| ---------- | --------- | ---------------------------- | ------- |
| `preserve` | `<div />` | `<div />`                    | .jsx    |
| `react`    | `<div />` | `React.createElement("div")` | .js     |

你可以通过在命令行里使用`--jsx`标记或 `tsconfig.json` 里的选项来指定模式。

> 具体配置文档：<https://www.tslang.cn/docs>。

设置完成后重跑项目，此时引入组件的方式发生变更：

```tsx
// 以前的引入方式：（使用相对路径）
import Comp from '../components/Comp'

// 现在的引入方式：（直接从components开始即可，当然，前提是components在src下）
import Comp from 'components/Comp'
```

### 2、函数式组件

`src` 下新建 `components` ，再在其中创建 `Comp1.tsx`：

```tsx
import React from 'react'

const Comp1: React.FC = function () {
    return (
        <>
            <h3>1</h3>
            <button>累加</button>
        </>
    )
}
export default Comp1;
```

然后在 `App.tsx` 中引入：

```tsx
import React from 'react'
import Comp1 from 'components/Comp1'

const App: React.FC = () => {
    return (
        <>
            <h2>你好世界</h2>
            <Comp1 />
        </>
    )
}

export default App;
```

> 其实不写React.FC也可以。React.FC表示：React.Function Component。React.FC 显式地定义了返回类型，其他方式是隐式推导的。

## 三、Hooks

函数式组件必备Hook，本教程结合TS一起讲解。

### 1、useState

`App.tsx` 中使用 useState 定义数据，以及修改数据的方法，并传递给 `Comp.tsx` 子组件：

```tsx
const [num, setNum] = useState(0);

<Comp1 num={num} />
```

子组件接收：

```tsx
import React from 'react'

const Comp1: React.FC = function (props) {
    return (
        <>
            <h3>{props.num}</h3>
            <button>累加</button>
        </>
    )
}
export default Comp1;
```

很明显，这么接收直接就报错。因为TS强制要求必须指定传参的字段及其类型，因此应当改为：

```tsx
import React from 'react'

const Comp1: React.FC = function (props: {num: number}) {
    return (
        <>
            <h3>{props.num}</h3>
            <button>累加</button>
        </>
    )
}
export default Comp1;
```

而实际上这是TS中接口的简化写法，完整点应该写为：

```tsx
import React from 'react'

interface IProps {
    num: number;
}

// 使用IProps接口定义字段类型
const Comp1: React.FC<IProps> = function (props) {
    return (
        <>
            <h3>{props.num}</h3>
            <button>累加</button>
        </>
    )
}

export default Comp1;
```

#### A. 事件直接父传子使用

目前 `setNum` 依然处于定义了但未使用的状态，因此ESlint又会一直给出提示，因此我们可以把这个累加的效果实现：

```tsx
// 父组件：
<Comp1 num={num} setNum={setNum} />

// 子组件
interface IProps {
    num: number;
    // 设定setNum为any
    setNum: any
}

<button onClick={()=>props.setNum(props.num+1)}>累加</button>
```

> 注意：
>
> 这里虽然设置为any可以实现累加，但不建议这么操作。

因此，真正的做法：

```tsx
import React from 'react'

interface IProps {
    num: number;
    setNum: (num:number)=>void;
}

// 使用IProps接口定义字段类型
const Comp1: React.FC<IProps> = function(props) {
    return (
        <>
            <h3>{props.num}</h3>
            <button onClick={()=>props.setNum(props.num+1)}>累加</button>
        </>
    )
}

export default Comp1;
```

#### B. 事件用子传父的做法

**\* 父组件**

```tsx
import React, {useState, useCallback} from 'react'
import Comp1 from 'components/Comp1'

const App: React.FC = () => {
    const [num, setNum] = useState(0)

	const toSetNum = (value: number) => setNum(value)

    return (
        <>
            <h2>你好世界</h2>
            <Comp1 num={num} toSetNum={()=>toSetNum} />
        </>
    )
}

export default App;
```

> setNum(newValue)：代表直接用新值替换初始值
>
> setNum(preValue => newValue)：代表用新值替换旧值

**\* 子组件**

```tsx
import React from 'react'

interface IProps {
    num: number;
    toSetNum: (num:number)=>void;
}

// 使用IProps接口定义字段类型
const Comp1: React.FC<IProps> = function(props) {
    return (
        <>
            <h3>{props.num}</h3>
            <button onClick={()=>props.toSetNum(props.num+1)}>累加</button>
        </>
    )
}

export default Comp1;
```

### 2、useEffect

React的Class Component中有 `componentDidMount`、 `componentDidUpdate` 和 `componentWillUnmount`，但Function Component并没有。

#### A、componentDidMount

```tsx
useEffect(()=>{
  console.log('componentDidMount')
}, [])	// 空数组表示不检测任何数据变化
```

#### B、comopnentDidUpdate

```tsx
useEffect(()=>{
  console.log('comopnentDidUpdate')
}, [num])	// 如果数组中包含了所有页面存在的字段，也可以直接不写
```

如果监听路由的变化：

```tsx
// 需要先安装路由，而且是react-router-dom@v6.x
useEffect(()=>{
  console.log('路由变化')
}, [location.pathname])
```

#### C、componentWillUnmount

```tsx
useEffect(()=>{
  return ()=>{
    // callback中的return代表组件销毁时触发的事件
  }
}, [])
```

### 3、memo、useMemo与useCallback

在Function Component中，也不再区分`mount`和`update`两个状态，这意味着函数组件的每一次调用都会执行内部的所有逻辑，就带来了非常大的性能损耗。`useMemo`和`useCallback`都是解决上述性能问题的。

来看下面这段代码：

```tsx
import React, { useState, useMemo, useCallback } from "react";

const Sub = () => {
  console.log("Sub被渲染了");	// 这行代码在父组件App2更新时，它也被迫一直更新
  return <h3>Sub组件</h3>;
};

export default function App2() {
  const [num, setNum] = useState<number>(0);

  const changeNum = () => setNum(num + 1)

  return (
    <div>
      <h2>num的值：{num}</h2>
      <button onClick={changeNum}>累加num</button>
      <Sub />
    </div>
  );
}
```

以上代码中可以测试出来，Sub组件的 `console.log` 在App2组件更新时，一直被迫触发，这就是典型的性能浪费。

#### A. memo

使用memo这个hook可以解决这一问题：

```tsx
import React, { useState, memo } from "react";

// Sub组件需要被memo包裹
const Sub = memo(() => {
    console.log("Sub被渲染了");
    return <h3>Sub组件</h3>;
  });

export default function App2() {
  const [num, setNum] = useState<number>(0);
    
  const changeNum = () => setNum(num + 1)

  return (
    <div>
      <h2>num的值：{num}</h2>
      <button onClick={changeNum}>累加num</button>
      <Sub />
    </div>
  );
}
```

> memo可以缓存组件，当组件的内容不受修改时，可以不更新该组件。

#### B. useCallback

但我们希望num的变化不造成Sub组件的更新：

```tsx
import React, { useState, memo, useCallback } from "react";

interface ISubProps {
  changeNum: () => void;
}

// Sub组件需要被memo包裹
const Sub = memo((props: ISubProps) => {
  console.log("Sub被渲染了");
  return (
    <>
      <button onClick={props.changeNum}>累加num</button>
      <h3>Sub组件</h3>
    </>
  );
});

export default function App2() {
  const [num, setNum] = useState<number>(0);

  // 将这个changeNum函数使用useCallback包裹一次
  const changeNum = useCallback(()=>{
      setNum((num)=>num+1)
  }, [])

  return (
    <div>
      <h2>num的值：{num}</h2>
      <Sub changeNum={changeNum} />
    </div>
  );
}
```

#### C. useMemo

useMemo与useCallback大致相同，只是useMemo需要在回调函数中再返回一个函数，我们称之为高阶函数：

```tsx
import React, { useState, memo, useMemo } from "react";

interface ISubProps {
  changeNum: () => void;
}

// Sub组件需要被memo包裹
const Sub = memo((props: ISubProps) => {
  console.log("Sub被渲染了");
  return (
    <>
      <button onClick={props.changeNum}>累加num</button>
      <h3>Sub组件</h3>
    </>
  );
});

export default function App2() {
  const [num, setNum] = useState<number>(0);

  // 将这个changeNum函数改为useMemo
  const changeNum = useMemo(() => {
    return () => setNum((num) => num + 1);
  }, []);

  return (
    <div>
      <h2>num的值：{num}</h2>
      <Sub changeNum={changeNum} />
    </div>
  );
}
```

### 4、自定义hook

React中的hook允许我们自定义，来尝试一个简单的：

> 自定义一个hook，将所有的小写字母改大写。

```tsx
import React from 'react'

const word = "Hello World";

function useBigWord(w: string){
    return w.toUpperCase();
}

const App3 = () => {
    const bigWord = useBigWord(word)
    return (
        <div>
            <h3>小写：{word}</h3>
            <h3>大写：{bigWord}</h3>
        </div>
    )
}

export default App3
```

## 四、React Redux

> 需求：使用TS+React Redux实现一个累加。

### A. 安装

```shell
$ yarn add redux react-redux redux-devtools-extension
```

### B. Store

src下新建 `store` 目录，在其中新建 `reducer.ts` 和 `index.ts`：

#### a. reducer

```ts
const defaultState = {
    num: 1
}

interface IAction {
    type: string;
    value: number;
}

// eslint-disable-next-line
export default (state=defaultState, action: IAction) => {
    let newState = JSON.parse(JSON.stringify(state));
    switch(action.type){
        case "increase":
            newState.num+=action.value;
            break;
        default:
            break;
    }
    return newState;
}
```

#### b. store

```tsx
import {applyMiddleware, createStore} from 'redux'
import reducer from './reducer'
import {composeWithDevTools} from 'redux-devtools-extension'

const store = createStore(reducer, composeWithDevTools(applyMiddleware()))
export default store;
```

### C. Provider

在入口文件 `index.tsx` 中：

```tsx
import ReactDOM from 'react-dom'
import App from './App4'
import {Provider} from 'react-redux'
import store from 'store'

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById("root")
)
```

### D. connect

在组件中：

```tsx
import {connect} from 'react-redux'
import React from 'react'
import {Dispatch} from 'redux'	// redux提供了Dispatch作为dispatch的类型检测接口

interface IProps {
    num: number;
    increaseFn: ()=>void
}

const App4: React.FC<IProps> = (props) => {
    return (
        <div>
            <h3>{props.num}</h3>
            <button onClick={()=>props.increaseFn()}>累加</button>
        </div>
    )
}

const mapStateToProps = (state: {num: number}) => {
    return {
        num: state.num
    }
}

const mapDispatchToProps = (dispatch: Dispatch) => {
    return {
        increaseFn(){
            dispatch({type: "increase", value: 2})
        }
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(App4)
```

## 五、路由

### 1、安装路由

本课程使用目前最新版的[React路由(v6)](https://reactrouter.com/)：

```shell
$ npm install react-router-dom@6
# 或者
$ yarn add react-router-dom@6
```

> 注意：
>
> 版本会随时更新，因此请指定版本安装。

### 2、路由配置

#### a. 路由创建

在 `src` 下创建 `router>index.tsx`。以首页与登录页切换为例：

```tsx
import App from "App6";
import Home from "Home";
import List from "List";
import Detail from "Detail";
import About from "About";
import Login from "Login";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

const MyRouter = () => (
  <Router>
    <Routes>
      <Route path="/" element={<App />}>
        <Route index element={<Home />}></Route>
        <Route path="/list" element={<List />}></Route>
        <Route path="/detail" element={<Detail />}></Route>
        <Route path="/about" element={<About />}></Route>
      </Route>
      <Route path="/login" element={<Login />}></Route>
    </Routes>
  </Router>
);

export default MyRouter;
```

> 关键词解析：
>
> 1、BrowserRouter重命名为Router
>
> 2、所有的Route组件必须放在Routes组件中
>
> 3、Route标签上的element属性必须填写标签结构的组件，如：，而不是 Home
>
> 4、加了index属性的路由不需要写path，因为/路径就指向该组件

#### b. 入口文件引入路由

`src>index.tsx` ：

```tsx
import ReactDOM from 'react-dom'
import MyRouter from 'router'

ReactDOM.render(
    <MyRouter />,
    document.getElementById("root")
)
```

#### c. 组件显示

`App.tsx` 中：

```tsx
import React from "react";
import { Outlet, Link } from "react-router-dom";

function App() {
  return (
    <div>
      <ul>
        <li><Link to={"/list"}>列表页</Link></li>
        <li><Link to={"/detail"}>详情页</Link></li>
        <li><Link to={"/about"}>关于我们</Link></li>
      </ul>
      <Outlet />
    </div>
  );
}

export default App;
```

> 关键词解析：
>
> 1、 组件用来显示子路由，类似于Vue的
>
> 2、Link最终会被html解析为a标签

**目前结合ts的情况下，无法使用index属性指定首页组件，因此如果希望 `/` 跳转 `/home`，需要：**

```tsx
import { useLocation } from "react-router-dom";

let { pathname } = useLocation();
useEffect(() => {
    if (pathname === "/") {
        navigate("/home");
    }
}, []);
```

### 3、参数获取

#### a. 子路由形式携带

路由跳转往往伴随着参数的传递，假如：

```tsx
// 登录页的路由配置
<Route path="/login/:id" element={<Login />}></Route>

// Link跳转路由
<Link to="/login/123">登录页</Link>
```

此时可以使用React Router Dom提供的Hook来获取：

```tsx
import { useParams } from 'react-router-dom'

// 从路由参数中解构出来
const {id} = useParams()
console.log(id)    // 123
```

#### b. 问号(?)形式参数

```tsx
// 登录页的路由配置
<Route path="/login" element={<Login />}></Route>

// Link跳转路由
<Link to="/login?id=123">登录页</Link>
```

获取形式：

```tsx
import { useSearchParams } from 'react-router-dom'

const [params] = useSearchParams()
console.log(params.getAll('id'))    // ['123']
```

> 以上的id其实属于携带方式不明确，也不一定会携带，因此路由可以设置为：
>
> ```tsx
> <Route path="/login/*" element={<Login />}></Route>
> ```

### 4、事件跳转

事件中执行跳转页面，可以使用useNavigate这个hook进行跳转。

```tsx
import { useNavigate } from "react-router-dom";

const navigate = useNavigate();
const goLogin = () => {
    navigate('/login')
}

<span onClick={goLogin}>登录页2</span>
```

简单参数的传递可以直接带在url后，而复杂参数需要以复杂数据类型的形式携带：

```tsx
const navigate = useNavigate();
navigate('/login', {state: {id: 456}})
```

> 注意：
>
> navigate方法第二个参数必须是对象，而且这个对象只接受replace和state两个属性，state可以用来携带参数。

携带复杂参数，可以使用useLocation来获取参数：

```tsx
const location = useLocation()
console.log(location.state.id);  // 456
```

> 注意：
>
> 这里如果使用了TS，那么location会报错，因为其中的state属于不确定的类型，因此没办法直接location.state调用。解决方法有两个：一是单独设置state字段为any，二是直接设置location类型为any。

```tsx
// 方法一：设置state为any
interface ILocation {
    state: any,
    search: string,
    pathname: string,
    key: string,
    hash: string
}

const location: ILocation = useLocation()

// 方法二：设置location为any
const location: any = useLocation()
```

### 5、404匹配

当路由为404时，可以对路由文件 `router/index.tsx` 进行如下匹配：

```tsx
...
import NoMatch from "NoMatch";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

const MyRouter = () => (
  <Router>
    <Routes>
      <Route path="/" element={<App />}>
        ...
      </Route>
      <Route path="/login" element={<Login />}></Route>
      <Route path="*" element={<NoMatch />}></Route>
    </Routes>
  </Router>
);

export default MyRouter;
```

如此，输入错误路径，就会自动重定向到404页面了。
