# 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页面了。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gb.akanote.cn/cms/part2-react+ts.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
