# Create React App requires Node 14 or higher.
npx create-react-app hook-ts --template typescript
# 出现 happy hacking 提示,通过命令 dir 可看到项目创建成功!
cd hook-ts
# 通过vscode打开
code .
# 退出外部命令行(git bash)
exit
# vscode 界面打开命令行 ctrl+` 后:
cd src
# 删除 src 下所有文件
rm *
# 创建index.ts与App.tsx
touch index.tsx App.tsx
# 返回根目录
cd ..
import React from 'react'
// 这里 App 组件后续不会再进行赋值操作,因此可用 const
// 普通函数 或 箭头函数 都可
// React.FC 表示:React.Function Component。
// React.FC 显式地定义了返回类型,其他方式是隐式的。
const App: React.FC = () => {
return (
<h2>hello world</h2>
)
}
export default App;
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App'
// 17 版本写法
// ReactDOM.render( , document.getElementById("root"))
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
注意:
- ts代表js,tsx代表jsx。
- 虽然 js 与 jsx 在React+TS中兼容,只是既然使用了ts,那就。。。
- 版本18 某些引入地方需要加
* as
。。。
# npm run start 中 run 可省
npm start
为方便后续开发,需要做一些常用 tsconfig 配置:
{
"compilerOptions": {
"target": "ESNext", // 配置ES语法
"baseUrl": "./src", // 配置引用基础路径
"jsx": "preserve", // 在preserve模式下生成的代码中会保留JSX以供后续的转换操作使用(如babel)
"allowSyntheticDefaultImports": true
}
}
模式 | 输入 | 输出 | 输出文件扩展名 |
---|---|---|---|
preserve | .jsx | ||
react | React.createElement(“div”) | .js |
也可以通过在命令行里使用–jsx来指定模式。
Module '"E:/Projects/shrm/hook-ts/node_modules/@types/react/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flag
Module '"E:/Projects/shrm/hook-ts/node_modules/@types/react-dom/client"' has no default export.
cd src
mkdir components
cd components
touch Comp1.tsx
import * as React from 'react'
const Comp1: React.FC = function () {
return (
<>
<h3>1</h3>
<button>累加</button>
</>
)
}
export default Comp1;
import * as React from 'react'
import Comp1 from 'components/Comp1'
// 这里 App 组件后续不会再进行赋值操作,因此可用 const
// 普通函数 或 箭头函数 都可
// React.FC表示:React.Function Component。
// React.FC 显式地定义了返回类型,其他方式是隐式的。
const App: React.FC = () => {
return (
<>
<h2>hello world</h2>
<Comp1 />
</>
)
}
export default App;
使用 useState 定义数据,以及修改数据的方法,并传递给子组件:
// App.tsx
const App: React.FC = () => {
const [num, setNum] = useState(0);
return (
<>
<h2>hello world</h2>
<Comp1 num={num}/>
</>
)
}
此时 num 会有红色下划波浪线:
不能将类型“{ num: number; }”分配给类型“IntrinsicAttributes”。
类型“IntrinsicAttributes”上不存在属性“num”。
子组件:
// Comp1.tsx
const Comp1: React.FC = function (props) {
return (
<>
<h3>{props.num}</h3>
<button>累加</button>
</>
)
}
num 也会有红色下划波浪线:
类型“{}”上不存在属性“num”。
因为TS强制要求必须指定传参的字段及其类型,因此应当改为:
// Comp1.tsx
const Comp1: React.FC = function (props: {num: number}) {
...
}
而对象类型一般都会声明为接口(interface ),完整写法:
// Comp1.tsx
interface IProps {
num: number
}
// 使用IProps接口定义字段类型(入参使用尖括号作为泛型跟在函数类型后)
const Comp1: React.FC<IProps> = function (props) {
...
}
事件也可以由 父组件直接传给子组件 使用:
// App.tsx
const App: React.FC = () => {
const [num, setNum] = useState(0);
return (
<>
<h2>hello world</h2>
<Comp1 num={num} setNum={setNum} />
</>
)
}
setNum出现红色下划波浪线:
不能将类型“{ num: number; setNum: Dispatch<SetStateAction<number>>; }”分配给类型“IntrinsicAttributes & IProps”。
类型“IntrinsicAttributes & IProps”上不存在属性“setNum”。
// Comp1.tsx
interface IProps {
num: number
}
// 使用IProps接口定义字段类型(入参使用尖括号作为泛型跟在函数类型后)
const Comp1: React.FC<IProps> = function (props) {
return (
<>
<h3>{ props.num }</h3>
<button onClick={()=>props.setNum(props.num+1)}>累加</button>
</>
)
}
setNum出现红色下划波浪线:
类型“IProps”上不存在属性“setNum”。
继续对 setNum 进行类型声明:
// Comp1.tsx
interface IProps {
num: number,
setNum: any
}
...
这样看似一切正常了,但是并不规范,规范写法:
// Comp1.tsx
interface IProps {
num: number,
setNum: (num: number) => void
}
...
函数类型声明标出入参类型以及返回值类型,无返回值标为 void,整体使用箭头函数形式
事件也可以由 子组件直接传给父组件 使用:
// App.tsx
const App: React.FC = () => {
const [num, setNum] = useState(0);
const toSetNum = (value: number) => setNum(value)
return (
<>
<h2>hello world</h2>
<Comp1 num={num} toSetNum={ toSetNum } />
</>
)
}
// Comp1.tsx
interface IProps {
num: number,
toSetNum: (num: number) => void
}
const Comp1: React.FC<IProps> = function (props) {
return (
<>
<h3>{ props.num }</h3>
<button onClick={ () => props.toSetNum(props.num + 1) }>累加</button>
</>
)
}
- setNum(newValue):代表直接用新值替换初始值
- setNum(preValue => newValue):代表用新值替换旧值
函数组件相对类组件的区别:
- 没有this
- 没有state
- 没有生命周期
相对于类组件,函数组件并没有生命周期函数,因此通过hook —— useEffect来模拟生命周期函数:
useEffect(()=>{
console.log('componentDidMount')
}, []) // 空数组表示不检测任何数据变化,即只在组件加载时执行一次
useEffect(()=>{
console.log('comopnentDidUpdate')
}, [num]) // 如果数组中包含了所有页面存在的字段,也可以直接不写第二个参数
如果监听路由的变化:
// 需要先安装路由,而且是react-router-dom@v6.x
useEffect(()=>{
console.log('路由变化')
}, [location.pathname])
useEffect(()=>{
return ()=>{
console.log('componentWillUnmount')
// callback中的return代表组件销毁时触发的事件
}
}, [])
这里可以看出,componentDidMount 和 componentWillUnmount 两个可以写到一起
用于性能优化的内置hooks
在函数组件中,不仅不会有机会通过 immutable 来提前判断是否需要调用相关“setState”,也不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。
const Sub = () => {
console.log("子组件被渲染了"); // 在父组件每次更新时,子组件也被迫更新
return <h3>子组件</h3>;
}
function App() {
const [num, setNum] = useState(0);
const changeNum = () => setNum(num + 1)
return (
<div>
<h2>{num}</h2>
<button onClick={changeNum}>累加</button>
<Sub />
</div>
);
}
useMemo 和useCallback 就是解决上述性能问题的。
import React, { useState, memo } from "react";
const Sub = memo(() => {
console.log("子组件被渲染了"); // 在父组件每次更新时,子组件也被迫更新
return <h3>子组件</h3>;
})
function App() { ... }
memo可以缓存组件,当组件内容未修改时,该组件不会重新渲染。
() => boolean
)来实现。export default memo(Sub)
将按钮放入子组件,数据显示在父组件
import React, { useState, memo, useCallback } from "react";
interface ISubProps {
changeNum: () => void;
}
// 子组件需要被memo包裹
const Sub = memo((props: ISubProps) => {
console.log("子组件被渲染了");
return (
<>
<button onClick={props.handleClick}>累加num</button>
<h3>子组件</h3>
</>
);
});
export default function App2() {
const [num, setNum] = useState<number>(0);
// 将函数使用useCallback包裹一次,再传给子组件,避免父组件state改变重新渲染引发子组件(未有依赖项改变)的重新渲染
const handleClick= useCallback(()=>{
// setNum(num+1) // 依赖初始值替换旧值,初始值不变,缓存无效
setNum((num)=>num+1) // 依赖旧值,新值替换旧值,缓存有效
}, [])
return (
<div>
<h2>{ num }</h2>
<Sub handleClick={ handleClick } />
</div>
);
}
- useCallback 的第二个参数放置依赖变量
- 当存在父子组件关系时,useCallback 必须搭配 memo 一起使用
useMemo与useCallback大致相同,只是函数外多嵌套一层返回函数,这种被称为高阶函数,在修改num的时候,返回上一次缓存的 changeNum 的值
import React, { useState, memo, useMemo } from "react";
interface ISubProps {
changeNum: () => void;
}
// 子组件需要被memo包裹
const Sub = memo((props: ISubProps) => {
console.log("子组件被渲染了");
return (
<>
<button onClick={props.changeNum}>累加num</button>
<h3>子组件</h3>
</>
);
});
export default function App() {
const [num, setNum] = useState<number>(0);
// 将这个changeNum函数改为useMemo,过程作为结果返回,等同于 useCallback,此案例无意义
const changeNum = useMemo(() => {
return () => setNum((num) => num + 1);
}, []);
return (
<div>
<h2>num的值:{num}</h2>
<Sub changeNum={changeNum} />
</div>
);
}
useMemo 的第二个参数放置依赖变量
- memo 用于缓存组件
- useCallback 用于缓存函数(过程)
- useMemo 用于缓存函数返回的数据对象(结果)
- 当子组件接收一个函数类型的props时,一般会使用useCallback来缓存这个函数,减少不必要的re-render。
- useMemo常用在以下两种场景的优化中:1)引用类型的变量 2)需要大量时间执行的计算函数。
yarn add redux react-redux redux-devtools-extension
src下新建 store 目录,在其中新建 reducer.ts 和 index.ts:
// store/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;
}
// store/index.ts
import {applyMiddleware, createStore} from 'redux'
import reducer from './reducer'
import {composeWithDevTools} from 'redux-devtools-extension'
const store = createStore(reducer, composeWithDevTools(applyMiddleware()))
export default store;
后发现“createStore”已弃用。。。改用 @reduxjs/toolkit 中的 configureStore:
npm install @reduxjs/toolkit
import {configureStore} from '@reduxjs/toolkit'
import reducer from './reducer'
// 创建一个 Redux store,并自动配置 Redux DevTools 扩展
export default configureStore({
reducer: reducer
})
相对单独的 reducer 文件,toolkit 中建议使用 createSlice
使用 Provider 包裹后,下面任意一级都可以使用 store 数据
// 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")
)
// App4.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: 1})
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App4)
- connect 是高阶函数
- 关于两个映射参数 mapStateToProps 和 mapDispatchToProps 见 reducer.ts 的定义位置
// src\store\numSlice.ts
import { createSlice } from '@reduxjs/toolkit';
interface IAction {
type: string;
payload: number;
}
export const numSlice = createSlice({
name: 'num',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
// Redux Toolkit 允许我们在 reducers 中编写 mutating 逻辑。
// 它实际上并没有 mutate state 因为它使用了 Immer 库,
// 它检测到草稿 state 的变化并产生一个全新的基于这些更改的不可变 state
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: IAction) => {
state.value += action.payload;
},
},
});
// 为每个 case reducer 函数生成 Action creators
export const { increment, decrement, incrementByAmount } = numSlice.actions;
export default numSlice.reducer;
// src\store\index.ts
import {configureStore} from '@reduxjs/toolkit'
import numSlice from './numSlice';
// 创建一个 Redux store,并自动配置 Redux DevTools 扩展
export default configureStore({
reducer: {
num: numSlice
}
})
// toolkitTest.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { decrement, increment } from './store/numSlice';
interface numState {
value: number
}
interface IState {
num: numState
}
export default function Num() {
const count = useSelector((state: IState) => state.num.value);
const dispatch = useDispatch();
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
);
}
npm install react-router-dom@6
# 或者
yarn add react-router-dom@6
在 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;
- 所有的Route组件必须放在Routes组件中
- Route标签上的element属性必须填写标签结构的组件,如:
,而不是 Home - 加了index属性的路由不需要写path,因为根路径(/)就指向该组件
- 6 之前版本的 Routes 是 Switch
- BrowserRouter 相当于路由模式中的 history 模式,url 不带 #
- HashRouter 相当于路由模式中的 hash 模式,url 带 #
import ReactDOM from 'react-dom'
import MyRouter from 'router'
ReactDOM.render(
<MyRouter />,
document.getElementById("root")
)
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;
- Outlet 组件用来显示子路由内容
- Link 最终会被 html 解析为 a 标签
目前结合ts的情况下,无法使用index属性指定首页组件,因此如果希望 / 跳转 /home,需要:
import { useLocation } from "react-router-dom";
let { pathname } = useLocation();
useEffect(() => {
if (pathname === "/") {
navigate("/home");
}
}, []);
路由跳转往往伴随着参数的传递,如:
// 登录页的路由配置
<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
// 登录页的路由配置
<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>
事件中执行跳转页面,可以使用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()
当路由为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页面了。
over