vite创建react项目
注意node.js版本,Vite 需要 Node.js 版本 20.19+, 22.12+。
npm create vite@latest
这里framework我们选择react
,语言选择typescript
npm i
安装依赖
npm run dev
启动项目
配置路径别名
1.安装node的类型声明
npm i -D @types/node
2.配置vite.config.json
ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { join } from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve:{
alias:{
'@':join(__dirname,'./src')
}
}
})
3.tsconfig.app.json和tsconfig.node.json 配置导入路径提示
ts
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
去掉StrictMode
mian.ts文件中注释掉StrictMode,因为它在开发阶段会导致纯函数式组件执行两次。
tsx
// <StrictMode>
// <App />
// </StrictMode>,
<App />
React.FC 函数式组件类型
jsx
// 这里的React.FC表示函数式组件类型
import React from "react"
const App:React.FC = () => {
return(
<h1>这是App组件</h1>
)
}
export default App
useState
让函数式组件拥有自己的状态
const [ 状态名,set函数 ] = useState(初始值)
1.基本语法案例
tsx
import React, { useState } from "react";
export const Count: React.FC = () => {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1)
}
return (
<>
<h1>count的值是:{count}</h1>
<button onClick={add}>加1</button>
</>
)
}
2.注意点
状态变化,必然会导致函数的重新执行(组件重新渲染)。
当函数式组件重新执行时,不会重复调用useState给函数赋予初始值,而是复用上次state值。
以函数的形式为状态赋予初始值
ts
const [value,setValue] = useState(()=>初始值)
代码案例:
ts
import React, { useState } from "react";
export const DateCom: React.FC = () => {
const [date] = useState(()=>{
const dt = new Date()
return {year:dt.getFullYear(),month:dt.getMonth() + 1,day:dt.getDate()}
})
return (
<>
<h1>当前的年月日信息</h1>
<p>年份:{date.year}</p>
<p>月份:{date.month}</p>
<p>日期:{date.day}</p>
</>
)
}
useState是异步变更状态
useState函数内部是以异步的形式修改状态的,所以修改状态后,无法立即拿到最新的状态
代码示例
tsx
export const Counter2: React.FC = () => {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1)
console.log(count) //打印获取不到最新的count值
}
return (
<>
<h1>count的值是:{count}</h1>
<button onClick={add}>加1</button>
</>
)
}
解决方法:
配合useEffect使用
tsx
// 依赖count值,回调函数会在count变化时执行
// 回调函数在组件首次渲染的时候也会执行
useEffect(()=>{
console.log(`count最新的值是:${count}`)
},[count])
解决更新不及时的问题
tsx
export const Counter3: React.FC = () => {
const [count, setCount] = useState(0);
const add = () => {
// setCount内部更新是异步的,但是这里连续两次调用的js是同步的
setCount(count + 1)
// 这里再次调用是,count的状态还有修改过来,还是0, setCount运行了两次,值还是1
setCount(count + 1)
}
return (
<>
<h1>count的值是:{count}</h1>
<button onClick={add}>加1</button>
</>
)
}
解决方法:
tsx
// 解决方法
...
// prev是上一次的旧值 setCount((prev)=>{ return prev + 1 })
setCount(prev => prev + 1)
setCount(prev => prev + 1)
...
当我们修改state状态时,如果发现是新值依赖与旧值,需要通过fn形参拿到旧值,再进行计算,并返回
更新对象类型的值
如果变更的类型是对象,需要使用展开运算符或者Object.assign()生成一个新对象,用新对象才能触发渲染
(旧对象即使值改变了,但是引用地址没变,react是检测不到的)
tsx
export const UserInfo: React.FC = () => {
const [user, setUser] = useState({
name: '张三',
age: 18,
gender: '男'
})
const edit = () => {
user.name = '李四'
user.age = 20
// setUser(user) //无效
// setUser({...user}) //有效
setUser(Object.assign({},user)) //有效
}
return (
<>
<h1>用户信息:</h1>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
<p>性别:{user.gender}</p>
<br />
<button onClick={edit}>修改用户信息</button>
</>
)
}
使用setState模拟组件强制刷新
tsx
export const FUpdate: React.FC = () => {
const [, forceUpdate] = useState({})
const update = () => {
// 每次传入一个新对象,组件就会刷新
forceUpdate({})
}
return (
<>
<h1>{Date.now()}</h1>
<button onClick={update}>强制刷新</button>
</>
)
}
useRef
获取DOM或组件的实例对象
存储渲染周期之间的共享数据
获取DOM
tsx
import React, { useRef } from "react";
export const InputFocus: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null)
const getFocus = () => {
inputRef.current?.focus()
}
return (
<>
<input type="text" ref={inputRef} />
<br />
<button onClick={getFocus}>获取焦点</button>
</>
)
}
存储渲染周期之间的共享数据
tsx
export const Counter: React.FC = () => {
const [count, setCount] = useState(0)
// useRef仅在函数第一次调用时执行,重新调用渲染时组件时,不会再次执行
const prevCount = useRef<number>(0)
const add = () => {
setCount((prev) => {
prevCount.current = prev
return prev + 1
})
}
return (
<div>
<h1>count的新值是:{count},旧值是:{prevCount.current}</h1>
<br />
<button onClick={add}>+1</button>
</div>
)
}
注意事项
- 1.
组件rerender时useRef不会被重复初始化
- 2.
ref.current变化时不会造成组件的rerender
案例:
tsx
export const RefTimer:React.FC = () => {
const [count, setCount] = useState(0)
const refTime = useRef<number>(Date.now())
const changeCount = () => {
// 变化能更新渲染
setCount(prev => prev + 1)
}
const changeTime = () => {
//变化并不能更新渲染
refTime.current = Date.now()
console.log(refTime.current)
}
// 1.useEffect 会在组件首次渲染完毕之后,默认执行一次
// 2.组件每次渲染完毕之后,会触发useEffect的回调函数,如果给了依赖项数组,则还要判断依赖项是否变化,再决定能否触发函数
useEffect(() => {
console.log("触发了useEffect的回调执行")
}, [refTime.current])
return (
<>
<h1>count的值是:{count}</h1>
<h1>refTime当前的值是:{refTime.current}</h1>
<button onClick={changeCount}>更改count</button>
<button onClick={changeTime}>更改refTime</button>
</>
)
}
React.forwardRef useImperativeHandle
假如想实现一个获取函数组件实例的操作:
父组件获取子组件
tsx
const Child: React.FC = () => {
const [count, setCount] = useState(0)
const add = (num:number)=>{
setCount(prev=>prev += num)
}
return (
<>
<h2>子组件的count值是:{count}</h2>
<button onClick={()=>add(1)}>+1</button>
<button onClick={()=>add(-1)}>-1</button>
</>
)
}
export const Father: React.FC = () => {
const childRef = useRef(null)
return (
<>
<h1>这是父组件</h1>
<hr />
{/* 这里直接获取会报错 */}
<Child ref={childRef} />
</>
)
}
要想获取函数式组件,我们需要额外以下几个步骤:
1.使用React.forwardRef包裹子组件
tsx
const Child = React.forwardRef(() => {
const [count, setCount] = useState(0)
const add = (num:number)=>{
setCount(prev=>prev += num)
}
return (
<>
<h2>子组件的count值是:{count}</h2>
<button onClick={()=>add(1)}>+1</button>
<button onClick={()=>add(-1)}>-1</button>
</>
)
})
2.useImperativeHandle获取子组件信息
useImperativeHandle返回的对象 就是childRef.current拿到的值
tsx
//useImperativeHandle返回的对象 就是childRef.current拿到的值
const Child = React.forwardRef((_,ref) => {
const [count, setCount] = useState(0)
const add = (num:number)=>{
setCount(prev=>prev += num)
}
// 这里我们返回一个对象
useImperativeHandle(ref,()=>{
return {
name:'张三',
age:18
}
})
return (
<>
<h2>子组件的count值是:{count}</h2>
<button onClick={()=>add(1)}>+1</button>
<button onClick={()=>add(-1)}>-1</button>
</>
)
})
export const Father: React.FC = () => {
const childRef = useRef(null)
// 打印子组件信息
const showMessage = ()=>{
console.log(childRef.current) // {name: '张三', age: 18}
}
return (
<>
<h1>这是父组件</h1>
<button onClick={showMessage}>获取子组件信息</button>
<hr />
<Child ref={childRef} />
</>
)
}
父组件重置子组件信息实战
tsx
const Child = React.forwardRef((_,ref) => {
const [count, setCount] = useState(0)
const add = (num:number)=>{
setCount(prev=>prev += num)
}
useImperativeHandle(ref,()=>{
// 暴露count,setCount
return {
count,
setCount
}
})
return (
<>
<h2>子组件的count值是:{count}</h2>
<button onClick={()=>add(1)}>+1</button>
<button onClick={()=>add(-1)}>-1</button>
</>
)
})
interface ChildRefMessage {
count:number,
setCount:(value:number)=>void
}
export const Father: React.FC = () => {
const childRef = useRef<ChildRefMessage | null>(null)
// 打印子组件信息
const showMessage = ()=>{
console.log(childRef.current)
}
// 重置子组件信息
const resetChild = ()=>{
childRef.current?.setCount(0)
}
return (
<>
<h1>这是父组件</h1>
<button onClick={showMessage}>获取子组件信息</button>
<button onClick={resetChild}>重置</button>
<hr />
<Child ref={childRef} />
</>
)
}
useImperativeHandle的第三个参数
useImperativeHandle(ref,createHandle,[deps])
- 参数一:父组件传递的ref
- 参数二:函数,返回想往外暴漏的属性
- 参数三:可选,函数依赖的值,有依赖值,才会更新暴漏的属性,假如 [] ,则不会更新暴露值。 不传的话,就会一直更新。
以下代码,我们依赖值为[],改变了count值后,父组件获取的值还是初始值0
tsx
const Child = React.forwardRef((_,ref) => {
const [count, setCount] = useState(0)
const add = (num:number)=>{
setCount(prev=>prev += num)
}
useImperativeHandle(ref,()=>{
// 暴露count,setCount
return {
count,
setCount
}
},[])
return (
<>
<h2>子组件的count值是:{count}</h2>
<button onClick={()=>add(1)}>+1</button>
<button onClick={()=>add(-1)}>-1</button>
</>
)
})
添加依赖项,获取更新后的数据:
tsx
// 此时父组件能够获取最新的count值
useImperativeHandle(ref,()=>{
// 暴露count,setCount
return {
count,
setCount
}
},[count])
useEffect
什么是函数的副作用
React函数的副作用就是 函数除了返回值外
对外界环境造成的其他影响
,即与组件渲染无关
的操作,例如:获取数据、修改全局变量、更新DOM等。
useEffect可以执行一写副作用,例如:请求数据、事件监听等。
useEffect(fn,[deps])
-
参数一:fn是一个副作用函数,函数会在每次
渲染完成
之后被调用。 -
参数二:可选,依赖项数组, 依赖项变化,重新执行fn副作用函数 依赖项没有变化时,则不会执行fn副作用函数
省略情况下:函数会在每次渲染完成之后被调用
没有依赖项的情况下:初始化渲染时会执行useEffect打印useEffect执行了!
,后面每次count变化,视图刷新都会执行useEffect打印useEffect执行了!
tsx
export const Count: React.FC = () => {
const [count, setCount] = useState(0)
const add = () => {
setCount(prev => prev += 1)
}
useEffect(()=>{
console.log("useEffect执行了!")
})
return (
<>
<h1>count的值是:{count}</h1>
<hr />
<button onClick={add}>+1</button>
</>
)
}
有依赖的情况下:
以下代码,仅当count的状态改变时,才会打印useEffect执行了!,flag状态改变,虽然刷新视图,但是不会再次执行打印。
tsx
export const Count: React.FC = () => {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const add = () => {
setCount(prev => prev += 1)
}
const toggle = () => {
setFlag(prev => !prev)
}
useEffect(() => {
console.log("useEffect执行了!")
}, [count])
return (
<>
<h1>count的值是:{count}</h1>
<h1>flag的值时:{ String(flag)}</h1>
<hr />
<button onClick={add}>+1</button>
<button onClick={toggle}>toggle</button>
</>
)
}
当依赖项是空数组的情况下,仅会在首次渲染完毕后默认执行一次。
注意事项
- 1.不要在useEffect中改变依赖项的值,会造成死循环
- 2.多个不同的副作用尽量分开声明,不要写在一个useEffect中
清理副作用
useEffect可以返回一个函数,用于清理副作用的回调
tsx
useEffect(()=>{
//1.执行副作用操作
//2.返回一个清理副作用的函数
return ()=>{/**执行清理操作 */}
},[依赖项])
典型应用场景:
- 1.组件卸载时,请求还未结束,清理请求。
- 2.组件中使用了定时器或绑定了监听事件,清除定时器或绑定事件
清理函数的触发时机:
- 1.组件被卸载的时候,会调用
- 2.当effect副作用函数被执行之前,会先执行清理函数
案例,卸载组件时清除事件监听:
tsx
const MouseMove: React.FC = () => {
const [position, setPosition] = useState({ x: 0, y: 0 })
const mouseMoveHandle = (e: MouseEvent) => {
console.log(e.clientX, e.clientY)
setPosition({ x: e.clientX, y: e.clientY })
}
useEffect(() => {
window.addEventListener('mousemove', mouseMoveHandle)
// 清除事件监听
return ()=>{ window.removeEventListener('mousemove', mouseMoveHandle) }
}, [])
return (
<>
<h1>鼠标的位置在:{JSON.stringify(position)}</h1>
</>
)
}
export const TestMouseMove: React.FC = () => {
const [flag, setFlag] = useState(true)
const toggle = () => {
setFlag(prev=>!prev)
}
return (
<>
<button onClick={toggle}>Toggle</button>
<hr />
{ flag && <MouseMove />}
</>
)
}
自定义封装Hooks案例
新建src/hooks/index.ts
获取鼠标位置自定义Hook
tsx
import {useState,useEffect} from 'react'
export const useMousePosition = (time: number = 0) => {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
let timerId: null | NodeJS.Timeout = null
const mouseMoveHandle = (e: MouseEvent) => {
if (timerId) return
timerId = setTimeout(() => {
console.log(e.clientX, e.clientY)
setPosition({ x: e.clientX, y: e.clientY })
timerId = null
}, time)
}
window.addEventListener('mousemove', mouseMoveHandle)
return () => { window.removeEventListener('mousemove', mouseMoveHandle) }
}, [])
return position
}
使用:
tsx
const MouseMove: React.FC = () => {
let position = useMousePosition(200)
return (
<>
<h1>鼠标的位置在:{JSON.stringify(position)}</h1>
</>
)
}
实现计时器Hook
实现基础功能:
1.10s后可以点击打印确认此协议
tsx
export const CounterDown: React.FC = () => {
const [count, setCount] = useState(10)
const [disabled, setDisabled] = useState(true)
const makeSure = () => {
console.log('确认此协议')
}
useEffect(() => {
let timerId = setTimeout(() => {
if (count > 1) {
setCount(prev => prev - 1)
} else {
setDisabled(false)
}
}, 1000)
return () => { clearTimeout(timerId) }
}, [count])
return (
<>
<button disabled={disabled} onClick={makeSure}> {disabled ? `请仔细阅读本协议(${count})秒` : '确认此协议'} </button>
</>
)
}
2.抽离功能函数
ts
// 定义函数类型
type UseCountDown = (time?:number)=>[number,boolean]
export const useCountDown:UseCountDown = (time:number = 10) => {
const [count, setCount] = useState(time)
const [disabled, setDisabled] = useState(true)
useEffect(() => {
let timerId = setTimeout(() => {
if (count > 1) {
setCount(prev => prev - 1)
} else {
setDisabled(false)
}
}, 1000)
return () => { clearTimeout(timerId) }
}, [count])
return [count,disabled]
}
3.引入使用
ts
import { useCountDown } from '@/hooks/index'
export const CounterDown: React.FC = () => {
const [count,disabled] = useCountDown(10)
const makeSure = () => {
console.log('确认此协议')
}
return (
<>
<button disabled={disabled} onClick={makeSure}> {disabled ? `请仔细阅读本协议(${count})秒` : '确认此协议'} </button>
</>
)
}
useLayoutEffect
与useEffect区别
用法与useEffect一样,区别在于,useEffect在DOM绘制之后触发,useLayoutEffect在DOM绘制之前触发
useLayoutEffect会阻策页面渲染
useReducer
当状态更新逻辑复杂时可以考虑使用useReducer,useReducer可以同时更新多个状态,而且把状态从组件中独立出来。
语法:
ts
const [ state,dispatch ] = useReducer(reducer,initState,initAction)
简单使用useReducer
tsx
import React, { useReducer } from "react";
type UserType = typeof defaultState
// 初始状态
const defaultState = { name: "zhangsan", age: 18 }
// prevState形参是上一次的旧状态
const reducer = (prevState:UserType) => {
return prevState
}
export const Father: React.FC = () => {
const [state] = useReducer(reducer,defaultState)
console.log(state) //{name: 'zhangsan', age: 18}
return (
<div>
<button>修改用户名</button>
<div className="father">
<Son1 />
<Son2 />
</div>
</div>
)
}
const Son1: React.FC = () => {
return (
<div className="son1">
</div>
)
}
const Son2: React.FC = () => {
return (
<div className="son2">
</div>
)
}
使用initAction 处理初始数据
tsx
import React, { useReducer } from "react";
type UserType = typeof defaultState
// 初始状态
const defaultState = { name: "zhangsan", age: -18 }
// prevState形参是上一次的旧状态
const reducer = (prevState: UserType) => {
return prevState
}
//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
// 处理age字段为正整数
return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}
export const Father: React.FC = () => {
const [state] = useReducer(reducer, defaultState,initAction)
console.log(state) //{name: 'zhangsan', age: 18}
return (
<div>
<button>修改用户名</button>
<div className="father">
<Son1 />
<Son2 />
</div>
</div>
)
}
const Son1: React.FC = () => {
return (
<div className="son1">
</div>
)
}
const Son2: React.FC = () => {
return (
<div className="son2">
</div>
)
}
dispatch reducer修改状态
tsx
import React, { useReducer } from "react";
type UserType = typeof defaultState
// 初始状态
const defaultState = { name: "zhangsan", age: -18 }
// prevState形参是上一次的旧状态
const reducer = (prevState: UserType, action) => {
console.log("reducer执行了")
switch (action.type) {
case "UPDATE_NAME":
return { ...prevState, name: action.payload }
default:
return prevState
}
}
//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
// 处理age字段为正整数
return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}
export const Father: React.FC = () => {
const [state, dispatch] = useReducer(reducer, defaultState, initAction)
console.log(state) //{name: 'zhangsan', age: 18}
const changeState = () => {
// 直接修改state,打印显示修改成功,但是页面不会更新渲染
// state.name = 'lisi'
// console.log(state)
// dispatch(信息对象)
dispatch({ type: "UPDATE_NAME", payload: "lisi" })
}
return (
<div>
<div>{JSON.stringify(state)}</div>
<button onClick={changeState}>修改用户名</button>
<div className="father">
<Son1 />
<Son2 />
</div>
</div>
)
}
const Son1: React.FC = () => {
return (
<div className="son1">
</div>
)
}
const Son2: React.FC = () => {
return (
<div className="son2">
</div>
)
}
使用props把父组件的数据传递给子组件使用
tsx
// Father
<div className="father">
<Son1 {...state}></Son1>
<Son2 {...state}></Son2>
</div>
tsx
// Son
const Son1: React.FC<UserType> = (props) => {
return (
<div className="son1">
<p>{JSON.stringify(props)}</p>
</div>
)
}
子组件修改父组件属性
点击子组件按钮,使年龄+1
tsx
import React, { useReducer } from "react";
type UserType = typeof defaultState
// 初始状态
const defaultState = { name: "zhangsan", age: -18 }
type ActionType = { type: "UPDATE_NAME", payload: string } | { type: "INCREMENT", payload: number }
// prevState形参是上一次的旧状态
const reducer = (prevState: UserType, action: ActionType) => {
console.log("reducer执行了")
switch (action.type) {
case "UPDATE_NAME":
return { ...prevState, name: action.payload }
case "INCREMENT":
return { ...prevState, age: prevState.age + action.payload }
default:
return prevState
}
}
//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
// 处理age字段为正整数
return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}
export const Father: React.FC = () => {
const [state, dispatch] = useReducer(reducer, defaultState, initAction)
console.log(state) //{name: 'zhangsan', age: 18}
const changeState = () => {
// 直接修改state,打印显示修改成功,但是页面不会更新渲染
// state.name = 'lisi'
// console.log(state)
// dispatch(信息对象)
dispatch({ type: "UPDATE_NAME", payload: "lisi" })
}
return (
<div>
<div>{JSON.stringify(state)}</div>
<button onClick={changeState}>修改用户名</button>
<div className="father">
<Son1 {...state} dispatch={dispatch}></Son1>
<Son2 {...state}></Son2>
</div>
</div>
)
}
const Son1: React.FC<UserType & {dispatch:React.Dispatch<ActionType>}> = (props) => {
const { dispatch, ...user } = props
const add = () => {
dispatch({type:"INCREMENT",payload:1})
}
return (
<div className="son1">
<p>{JSON.stringify(user)}</p>
<button onClick={add}>年龄+1</button>
</div>
)
}
const Son2: React.FC<UserType> = (props) => {
return (
<div className="son2">
<p>{JSON.stringify(props)}</p>
</div>
)
}
在GrandSon组件中重置数据案例
tsx
import React, { useReducer } from "react";
type UserType = typeof defaultState
// 初始状态
const defaultState = { name: "zhangsan", age: -18 }
type ActionType = { type: "UPDATE_NAME", payload: string } | { type: "INCREMENT", payload: number } | { type: "RESET" }
// prevState形参是上一次的旧状态
const reducer = (prevState: UserType, action: ActionType) => {
console.log("reducer执行了")
switch (action.type) {
case "UPDATE_NAME":
return { ...prevState, name: action.payload }
case "INCREMENT":
return { ...prevState, age: prevState.age + action.payload }
case "RESET":
return defaultState
default:
return prevState
}
}
//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
// 处理age字段为正整数
return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}
export const Father: React.FC = () => {
const [state, dispatch] = useReducer(reducer, defaultState, initAction)
console.log(state) //{name: 'zhangsan', age: 18}
const changeState = () => {
// 直接修改state,打印显示修改成功,但是页面不会更新渲染
// state.name = 'lisi'
// console.log(state)
// dispatch(信息对象)
dispatch({ type: "UPDATE_NAME", payload: "lisi" })
}
return (
<div>
<div>{JSON.stringify(state)}</div>
<button onClick={changeState}>修改用户名</button>
<div className="father">
<Son1 {...state} dispatch={dispatch}></Son1>
<Son2 {...state} dispatch={dispatch}></Son2>
</div>
</div>
)
}
const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {
const { dispatch, ...user } = props
const add = () => {
dispatch({ type: "INCREMENT", payload: 1 })
}
return (
<div className="son1">
<p>{JSON.stringify(user)}</p>
<button onClick={add}>年龄+1</button>
</div>
)
}
const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {
return (
<div className="son2">
<p>{JSON.stringify(props)}</p>
<GrandSon dispatch={props.dispatch} />
</div>
)
}
const GrandSon: React.FC<{dispatch:React.Dispatch<ActionType>}> = (props) => {
const reset = () => {
props.dispatch({type:"RESET"})
}
return (
<>
<h3>这是GrandSon组件</h3>
<button onClick={reset}>重置</button>
</>
)
}
使用Immer编写更简洁的reducer更新逻辑
npm install immer use-immer -S
tsx
import { useImmerReducer } from 'use-immer'
useImmerReducer可以之替换useReducer,使用和useReducer一样的功能
const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)
// reducer修改状态
const reducer = (prevState: UserType, action: ActionType) => {
console.log("reducer执行了")
switch (action.type) {
case "UPDATE_NAME":
// return { ...prevState, name: action.payload }
prevState.name = action.payload
break
case "INCREMENT":
// return { ...prevState, age: prevState.age + action.payload }
prevState.age += action.payload
break
case "RESET":
return defaultState
default:
return prevState
}
}
useContext
在react函数式组件中,如果组件的嵌套层级很深,当父组件想把数据共享给深层的子组件,传统的方法是使用props一层层把数据向下传递。
使用props层层传递数据的维护性太差,可以使用 React.createContext() + useContext()轻松实现多层组件的数据传递。
使用步骤如下:
- 1.在全局下创建Context对象
- 2.在父组件中使用Context.Provider提供数据
- 3.在子组件中使用useContext
基础用法示例:
tsx
import React, { useState, useContext } from "react";
type AppContextType = { count: number, setCount: React.Dispatch<React.SetStateAction<number>> }
//1.创建context对象
const AppContext = React.createContext<AppContextType>({} as AppContextType)
export const LevelA: React.FC = () => {
const [count, setCount] = useState(0)
return (
<div className="levelA">
<h3>count的值是:{count}</h3>
<button onClick={() => { setCount(prev => prev + 1) }}>+1</button>
{/* 2.AppContext.Provider提供数据 */}
<AppContext.Provider value={{ count, setCount }}>
<LevelB />
</AppContext.Provider>
</div>
)
}
const LevelB: React.FC = () => {
return (
<div className="levelB">
<LevelC />
</div>
)
}
const LevelC: React.FC = () => {
const ctx = useContext(AppContext)
const add = () => {
ctx.setCount((prev) => prev + 1)
}
const reset = ()=>{
ctx.setCount(() => 0)
}
return (
<div className="levelC">
<p>count的值是:{ctx.count}</p>
<button onClick={add}>+1</button>
<button onClick={reset}>重置</button>
</div>
)
}
***以非侵入的方式使用Context
创建wrapper组件:
tsx
import React, { useState, useContext } from "react";
type AppContextType = { count: number, setCount: React.Dispatch<React.SetStateAction<number>> }
//1.创建context对象
const AppContext = React.createContext<AppContextType>({} as AppContextType)
// 创建一个wrapper组件
export const AppContextWrapper:React.FC<React.PropsWithChildren> = (props)=>{
const [count, setCount] = useState(0)
return(
// {props.children} 这里使用了插槽 即是 levelA
<AppContext.Provider value={{ count, setCount }}>{props.children}</AppContext.Provider>
)
}
export const LevelA: React.FC = () => {
const ctx = useContext(AppContext)
return (
<div className="levelA">
<h3>count的值是:{ctx.count}</h3>
<button onClick={() => { ctx.setCount(prev => prev + 1) }}>+1</button>
<LevelB />
</div>
)
}
const LevelB: React.FC = () => {
return (
<div className="levelB">
<LevelC />
</div>
)
}
const LevelC: React.FC = () => {
const ctx = useContext(AppContext)
const add = () => {
ctx.setCount((prev) => prev + 1)
}
const reset = ()=>{
ctx.setCount(() => 0)
}
return (
<div className="levelC">
<p>count的值是:{ctx.count}</p>
<button onClick={add}>+1</button>
<button onClick={reset}>重置</button>
</div>
)
}
使用:
tsx
import React from "react"
import { AppContextWrapper, LevelA } from "@/components/useContext/base"
const App: React.FC = () => {
return (
<AppContextWrapper>
<LevelA />
</AppContextWrapper>
)
}
export default App
React.memo useMemo
React.memo缓存组件
以下代码,当父组件中引用子组件时,父组件更新,连带着子组件也会更新,而子组件仅仅是用上了父组件穿过来的count。
tsx
// 优化前
import React, { useEffect, useState } from "react";
export const Father:React.FC = () => {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const add = () => {
setCount(prev => prev + 1)
}
const toggle = () => {
setFlag(prev=>!prev)
}
return (
<div>
<h1>父组件</h1>
<h3>count的值是:{count}</h3>
<h3>flag的值是:{String(flag)}</h3>
<button onClick={add}>+1</button>
<button onClick={toggle}>Toggle</button>
<Son count={count} />
</div>
)
}
const Son:React.FC<{count:number}> = ({ count }) => {
useEffect(() => {
console.log("触发了子组件的渲染")
})
return (
<div>
<h1>子组件</h1>
<h3>count的值是:{count}</h3>
</div>
)
}
假如子组件需要仅仅在count值改变时,更新,而flag这个没用上的值改变时不更新,就可以使用React.memo来实现。
tsx
// 优化后:
// 使用React.memo包裹子组件即可
const Son:React.FC<{count:number}> = React.memo(({ count }) => {
useEffect(() => {
console.log("触发了子组件的渲染")
})
return (
<div>
<h1>子组件</h1>
<h3>count的值是:{count}</h3>
</div>
)
})
useMemo缓存计算结果
useMemo是React中的一个Hook,用于在组件渲染期间缓存计算结果,避免不必要的重复计算,从而优化性能
ts
const memoValue = useMemo(()=>{
return 计算的值
},[value]) //表示监听value变化
依赖项:
- 不传,每次更新都会执行
-
\],只会计算一次
Father中的以下代码,tips计算仅仅与flag有关,但是我们在修改count的值时,重新渲染组件时,也会执行tips计算,即更新组件就是进行tips计算。
tsx
// 优化前
export const Father: React.FC = () => {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const add = () => {
setCount(prev => prev + 1)
}
const toggle = () => {
setFlag(prev => !prev)
}
//根据布尔值进行计算 动态返回内容
const tips = () => {
console.log("触发了tips的重新计算")
return flag ? "上班" : "下班"
}
return (
<div>
<h1>父组件</h1>
<h3>count的值是:{count}</h3>
<h3>flag的值是:{String(flag)}</h3>
<h1>{tips()}</h1>
<button onClick={add}>+1</button>
<button onClick={toggle}>Toggle</button>
<Son count={count} />
</div>
)
}
使用useMemo缓存计算
tsx
export const Father: React.FC = () => {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const add = () => {
setCount(prev => prev + 1)
}
const toggle = () => {
setFlag(prev => !prev)
}
//根据布尔值进行计算 动态返回内容
const tips = useMemo(() => {
console.log("触发了tips的重新计算")
return flag ? "上班" : "下班"
},[flag])
return (
<div>
<h1>父组件</h1>
<h3>count的值是:{count}</h3>
<h3>flag的值是:{String(flag)}</h3>
<h1>{tips}</h1>
<button onClick={add}>+1</button>
<button onClick={toggle}>Toggle</button>
<Son count={count} />
</div>
)
}
useCallback
useMemo
是缓存某个变量值的效果 useCallback
用来对组件内的函数进行缓存,它返回的事缓存的函数
ts
const memoCallback = useCallback(cb,array)
useCallback会返回一个memorized回调函数供组件使用
- 1.cb是一个函数,用于处理业务逻辑,这个cb就是需要被缓存的函数
- 2.array是依赖项数组,当array中的依赖项发生变化时才会重新执行useCallback
-- 省略array,每次更新都会重新计算
-- array为空数组,则会在组件第一次初始化时计算一次
-- array不为空,只有在依赖项变化才会执行
tsx
//优化前:
import React, { useState } from "react";
export const Search: React.FC = () => {
const [kw,setKw] = useState("")
// 这里只要输入一个字符 就会创建一次onKwChange方法
const onKwChange = (e:React.ChangeEvent<HTMLInputElement>)=>{
setKw(e.currentTarget.value)
}
return (
<>
<input type="text" value={kw} onChange={onKwChange} />
<hr />
<p>{kw}</p>
</>
)
}
tsx
//优化后:
export const Search: React.FC = () => {
const [kw, setKw] = useState("")
// useCallback缓存函数 避免重新创建
const onKwChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setKw(e.currentTarget.value)
},[])
return (
<>
<input type="text" value={kw} onChange={onKwChange} />
<hr />
<p>{kw}</p>
</>
)
}
useTransition
useTransition
可以将一个更新转为低优先级 更新,不阻塞UI对用户操作的响应
tsx
function TabContainer(){
const [ isPending,startTransition ] = useTransition();
}
- 调用useTransition不需要任何参数 返回值:
- isPending 布尔值:是否存在待处理的transition,如果值为true,说明页面上存在待渲染的部分,可以提示用户页面正在加载
- startTransition 函数:调用此函数,可以把状态的更新标记为低优先级的,不阻塞UI对用户的响应
注意事项 1.传递给startTransition的函数必须是同步
的,React会立即执行此函数。(startTransition() 中代码不能是异步的) 2.被标记为transition的状态更新将被其他状态更新打断。(低优先级渲染,切换为其他渲染,会打断低优先级渲染)
使用案例: 以下代码,当鼠标悬浮在按钮上会有样式变化,当我们切换到Movie标签时,由于Movie标签下渲染了一万条数据,所以切换过程中,在将鼠标悬浮在别的tab,不会立即变化,而是等Movie标签下渲染完毕了,才有样式变化, 这样会造成卡顿现象,也就是页面渲染渲染阻塞了用户交互
tsx
import React, { useState } from "react";
export const TabContainer: React.FC = () => {
const [activeTab, setActiveTab] = useState('home')
const onBtnClick = (name: string) => {
setActiveTab(name)
}
return (
<div style={{ height: '500px' }}>
<TabButton isActive={activeTab === 'home'} onClick={() => { onBtnClick('home') }}>Home</TabButton>
<TabButton isActive={activeTab === 'movie'} onClick={() => { onBtnClick('movie') }}>Movie</TabButton>
<TabButton isActive={activeTab === 'about'} onClick={() => { onBtnClick('about') }}>About</TabButton>
<hr />
{activeTab === 'home' && <HomeTab />}
{activeTab === 'movie' && <MovieTab />}
{activeTab === 'about' && <AboutTab />}
</div>
)
}
const TabButton: React.FC<React.PropsWithChildren & { onClick: () => void; isActive: boolean }> = (props) => {
return (
<>
<button className={['btn', props.isActive ? 'active' : ''].join(" ")} onClick={props.onClick}>{props.children}</button>
</>
)
}
const HomeTab: React.FC = () => {
return (
<>
HomeTab
</>
)
}
const MovieTab: React.FC = () => {
const items = Array(10000).fill("MovieTab").map((item,index)=>{
return <p key={index}>{item}</p>
})
return items
}
const AboutTab: React.FC = () => {
return (
<>
AboutTab
</>
)
}
css
.btn{
border: 1px solid #fff;
margin: 5px;
padding: 10px;
background-color: rgb(8, 92, 238);
color: #fff;
transition: opacity 0.5s ease;
}
.btn:hover{
opacity: 0.6;
transition: opacity 0.5s ease;
}
.btn.active{
background-color: rgb(3, 150, 0);
}
优化
将切换tab时渲染变为低优先级:
关键代码:
tsx
const [,startTransition] = useTransition()
const onBtnClick = (name: string) => {
// 把某次更新,标记为低优先级的
startTransition(()=>{
setActiveTab(name)
})
}
isPending实现页面loading
tsx
import React, { useState, useTransition } from "react";
export const TabContainer: React.FC = () => {
const [activeTab, setActiveTab] = useState('home')
const [isPending, startTransition] = useTransition()
const onBtnClick = (name: string) => {
// 把某次更新,标记为低优先级的
startTransition(() => {
setActiveTab(name)
})
}
// isPending为true则展示loading
const Tab = () => {
if (isPending) {
return <h3>loading...</h3>
}
switch (activeTab) {
case 'home':
return <HomeTab />
case 'movie':
return <MovieTab />
case 'about':
return <AboutTab />
}
}
return (
<div style={{ height: '500px' }}>
<TabButton isActive={activeTab === 'home'} onClick={() => { onBtnClick('home') }}>Home</TabButton>
<TabButton isActive={activeTab === 'movie'} onClick={() => { onBtnClick('movie') }}>Movie</TabButton>
<TabButton isActive={activeTab === 'about'} onClick={() => { onBtnClick('about') }}>About</TabButton>
<hr />
{Tab()}
</div>
)
}
const TabButton: React.FC<React.PropsWithChildren & { onClick: () => void; isActive: boolean }> = (props) => {
return (
<>
<button className={['btn', props.isActive ? 'active' : ''].join(" ")} onClick={props.onClick}>{props.children}</button>
</>
)
}
const HomeTab: React.FC = () => {
return (
<>
HomeTab
</>
)
}
const MovieTab: React.FC = () => {
const items = Array(10000).fill("MovieTab").map((item, index) => {
return <p key={index}>{item}</p>
})
return items
}
const AboutTab: React.FC = () => {
return (
<>
AboutTab
</>
)
}
useDeferredValue
使用useDeferredValue可以让状态延迟更新
以下案例,useDeferredValue + React.memo实现输入框查询效果,类似防抖,但是和防抖有些区别:
与防抖或节流不同,useDeferredValue 不需要选择任何固定延迟时间。如果用户的设备很快(比如性能强劲的笔记本电脑),延迟的重渲染几乎会立即发生并且不会被察觉。如果用户的设备较慢,那么列表会相应地"滞后"于输入,滞后的程度与设备的速度有关。
此外,与防抖或节流不同,useDeferredValue 执行的延迟重新渲染默认是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,但用户进行了另一次键盘输入,React 会放弃该重新渲染,先处理键盘输入,然后再次开始在后台渲染。相比之下,防抖和节流仍会产生不顺畅的体验,因为它们是阻塞的:它们仅仅是将渲染阻塞键盘输入的时刻推迟了。
tsx
//未优化前,输入一个字符就立即渲染一下,会有卡顿效果
import React, { useState } from "react";
export const SearchList: React.FC = () => {
const [text, setText] = useState('')
const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value)
}
return (
<>
<input type="text" value={text} onChange={(e) => { changeText(e) }} />
<hr />
<ShowList text={text} />
</>
)
}
const ShowList: React.FC<{text:string}> = (props) => {
return Array(10000).fill(props.text).map((item,index)=>{
return <p key={index}>{item}</p>
})
}
tsx
// 优化后
import React, { useDeferredValue, useEffect, useState } from "react";
export const SearchList: React.FC = () => {
const [text, setText] = useState('')
// deferredText会延迟更新
const deferredText = useDeferredValue(text)
const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value)
}
useEffect(()=>{
// 这里打印,能看出text和deferredText的差异
console.log(deferredText,"deferredText"),
console.log(text,'text')
},[text])
return (
<>
<input type="text" value={text} onChange={(e) => { changeText(e) }} />
<hr />
<ShowList text={deferredText} />
</>
)
}
// React.memo缓存,deferredText更新后组件才会重新渲染
const ShowList: React.FC<{text:string}> = React.memo((props) => {
return Array(10000).fill(props.text).map((item,index)=>{
return <p key={index}>{item}</p>
})
})