一、项目创建与准备
1、创建项目
创建名为 hook-ts
的项目:
$ 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用户:
$ cd src
$ rm *
# 创建index.ts与App.tsx
$ touch index.ts App.tsx
# 返回根目录
$ cd ..
win用户手动删或执行 del *
可删除src下的文件,或者打开 git bash
也可操作以上命令。
3、App组件
任意给 App.tsx
写点内容:
import React from 'react'
const App = () => {
return (
<h2>你好世界</h2>
)
}
export default App;
4、项目入口文件
在 index.ts
中:
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<App />,
document.getElementById("root")
)
但是你会发现一直报错:
原因是ts文件会被解析为js,而React+TS的项目是不允许ts成为一个组件的,因此只需要将 index.ts
改为 index.tsx
即可。
5、项目运行
最后,项目根目录下运行:
即可将项目运行。
二、Function Component
1、tsconfig
项目中如果每次都使用相对路径引入组件,就比较麻烦,这里推荐配置 tsconfig.json
:
{
"compilerOptions": {
"target": "ESNext", // 配置ES语法
"baseUrl": "./src", // 配置基础路径
"jsx": "preserve" // 在preserve模式下生成的代码中会保留JSX以供后续的转换操作使用(如babel)
}
}
修改以上这几项,其中jsx的配置解释:
React.createElement("div")
你可以通过在命令行里使用--jsx
标记或 tsconfig.json
里的选项来指定模式。
具体配置文档:https://www.tslang.cn/docs。
设置完成后重跑项目,此时引入组件的方式发生变更:
// 以前的引入方式:(使用相对路径)
import Comp from '../components/Comp'
// 现在的引入方式:(直接从components开始即可,当然,前提是components在src下)
import Comp from 'components/Comp'
2、函数式组件
src
下新建 components
,再在其中创建 Comp1.tsx
:
import React from 'react'
const Comp1: React.FC = function () {
return (
<>
<h3>1</h3>
<button>累加</button>
</>
)
}
export default Comp1;
然后在 App.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
子组件:
const [num, setNum] = useState(0);
<Comp1 num={num} />
子组件接收:
import React from 'react'
const Comp1: React.FC = function (props) {
return (
<>
<h3>{props.num}</h3>
<button>累加</button>
</>
)
}
export default Comp1;
很明显,这么接收直接就报错。因为TS强制要求必须指定传参的字段及其类型,因此应当改为:
import React from 'react'
const Comp1: React.FC = function (props: {num: number}) {
return (
<>
<h3>{props.num}</h3>
<button>累加</button>
</>
)
}
export default Comp1;
而实际上这是TS中接口的简化写法,完整点应该写为:
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又会一直给出提示,因此我们可以把这个累加的效果实现:
// 父组件:
<Comp1 num={num} setNum={setNum} />
// 子组件
interface IProps {
num: number;
// 设定setNum为any
setNum: any
}
<button onClick={()=>props.setNum(props.num+1)}>累加</button>
注意:
这里虽然设置为any可以实现累加,但不建议这么操作。
因此,真正的做法:
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. 事件用子传父的做法
* 父组件
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):代表用新值替换旧值
* 子组件
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
useEffect(()=>{
console.log('componentDidMount')
}, []) // 空数组表示不检测任何数据变化
B、comopnentDidUpdate
useEffect(()=>{
console.log('comopnentDidUpdate')
}, [num]) // 如果数组中包含了所有页面存在的字段,也可以直接不写
如果监听路由的变化:
// 需要先安装路由,而且是react-router-dom@v6.x
useEffect(()=>{
console.log('路由变化')
}, [location.pathname])
C、componentWillUnmount
useEffect(()=>{
return ()=>{
// callback中的return代表组件销毁时触发的事件
}
}, [])
3、memo、useMemo与useCallback
在Function Component中,也不再区分mount
和update
两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。useMemo
和useCallback
都是解决上述性能问题的。
来看下面这段代码:
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可以解决这一问题:
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组件的更新:
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需要在回调函数中再返回一个函数,我们称之为高阶函数:
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,将所有的小写字母改大写。
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. 安装
$ yarn add redux react-redux redux-devtools-extension
B. Store
src下新建 store
目录,在其中新建 reducer.ts
和 index.ts
:
a. reducer
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
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
中:
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
在组件中:
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):
$ npm install react-router-dom@6
# 或者
$ yarn add react-router-dom@6
注意:
版本会随时更新,因此请指定版本安装。
2、路由配置
a. 路由创建
在 src
下创建 router>index.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
:
import ReactDOM from 'react-dom'
import MyRouter from 'router'
ReactDOM.render(
<MyRouter />,
document.getElementById("root")
)
c. 组件显示
App.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
,需要:
import { useLocation } from "react-router-dom";
let { pathname } = useLocation();
useEffect(() => {
if (pathname === "/") {
navigate("/home");
}
}, []);
3、参数获取
a. 子路由形式携带
路由跳转往往伴随着参数的传递,假如:
// 登录页的路由配置
<Route path="/login/:id" element={<Login />}></Route>
// Link跳转路由
<Link to="/login/123">登录页</Link>
此时可以使用React Router Dom提供的Hook来获取:
import { useParams } from 'react-router-dom'
// 从路由参数中解构出来
const {id} = useParams()
console.log(id) // 123
b. 问号(?)形式参数
// 登录页的路由配置
<Route path="/login" element={<Login />}></Route>
// Link跳转路由
<Link to="/login?id=123">登录页</Link>
获取形式:
import { useSearchParams } from 'react-router-dom'
const [params] = useSearchParams()
console.log(params.getAll('id')) // ['123']
以上的id其实属于携带方式不明确,也不一定会携带,因此路由可以设置为:
<Route path="/login/*" element={<Login />}></Route>
4、事件跳转
事件中执行跳转页面,可以使用useNavigate这个hook进行跳转。
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
const goLogin = () => {
navigate('/login')
}
<span onClick={goLogin}>登录页2</span>
简单参数的传递可以直接带在url后,而复杂参数需要以复杂数据类型的形式携带:
const navigate = useNavigate();
navigate('/login', {state: {id: 456}})
注意:
navigate方法第二个参数必须是对象,而且这个对象只接受replace和state两个属性,state可以用来携带参数。
携带复杂参数,可以使用useLocation来获取参数:
const location = useLocation()
console.log(location.state.id); // 456
注意:
这里如果使用了TS,那么location会报错,因为其中的state属于不确定的类型,因此没办法直接location.state调用。解决方法有两个:一是单独设置state字段为any,二是直接设置location类型为any。
// 方法一:设置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
进行如下匹配:
...
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页面了。