上期在这~
ReactHooks【二】
一.useReducer
1.1 useReducer 的语法格式
useReducer 的基础语法如下:
const [state, dispatch] = useReducer(reducer, initState, initAction?)
其中:
- reducer 是一个函数,类似于 (prevState, action) => newState。形参 prevState 表示旧状态,形参 action 表示本次的行为,返回值 newState 表示处理完毕后的新状态。
- initState 表示初始状态,也就是默认值。
- initAction 是进行状态初始化时候的处理函数,它是可选的,如果提供了 initAction 函数,则会把 initState 传递给 initAction 函数进行处理,initAction 的返回值会被当做初始状态。
- 返回值 state 是状态值。dispatch 是更新 state 的方法,让他接收 action 作为参数,useReducer 只需要调用 dispatch(action) 方法传入的 action 即可更新 state。
1.2 定义组件的基础结构
定义名为 Father 的父组件如下:
import React from 'react'
// 父组件
export const Father: React.FC = () => {
return (
<div>
<button>修改 name 的值</button>
<div className="father">
<Son1 />
<Son2 />
</div>
</div>
)
}
定义名为 Son1 和 Son2 的两个子组件如下:
// 子组件1
const Son1: React.FC = () => {
return <div className="son1"></div>
}
// 子组件2
const Son2: React.FC = () => {
return <div className="son2"></div>
}
在 index.css 中添加对应的样式:
.father {
display: flex;
justify-content: space-between;
width: 100vw;
}
.son1 {
background-color: orange;
min-height: 300px;
flex: 1;
padding: 10px;
}
.son2 {
background-color: lightblue;
min-height: 300px;
flex: 1;
padding: 10px;
}
1.3 定义 useReducer 的基础结构
1.3.1按需导入 useReducer 函数
import React, { useReducer } from ‘react’
1.3.2定义初始数据
const defaultState = { name: ‘test’, age: 16 }
1.3.3 定义 reducer 函数根据旧状态,进行一系列处理,最终返回新状态:
const reducer = (prevState) => {
console.log('触发了 reducer 函数')
return prevState
}
1.3.4 在 Father 组件中,调用 useReducer(reducerFn, 初始状态) 函数,并得到 reducer 返回的状态
// 父组件
export const Father: React.FC = () => {
// useReducer(fn, 初始数据, 对初始数据进行处理的fn)
const [state] = useReducer(reducer, defaultState)
console.log(state)
return (
<div>
<button>修改 name 的值</button>
<div className="father">
<Son1 />
<Son2 />
</div>
</div>
)
}
1.3.5为 reducer 中的 initState 指定数据类型
在 Father 组件中使用 state 时,就可以出现类型的智能提示啦
// 定义状态的数据类型
type UserType = typeof defaultState
const defaultState = { name: 'test', age: 16 }
// 给 initState 指定类型为 UserType
const reducer = (prevState: UserType) => {
console.log('触发了 reducer 函数')
return prevState
}
1.4 使用 initAction 处理初始数据
1.4.1 定义名为 initAction 的处理函数
如果初始数据中的 age 为小数、负数、或 0 时,对 age 进行非法值的处理:
//形参:初始状态
//返回值:处理好的初始状态
const initAction = (initState: UserType) => {
// 把 return 的对象,作为 useReducer 的初始值
return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}
1.4.2 在 Father 组件中,使用步骤1声明的 initAction 函数如下:
// 父组件
export const Father: React.FC = () => {
// useReducer(fn, 初始数据, 对初始数据进行处理的fn)
const [state] = useReducer(reducer, defaultState, initAction)
// 省略其它代码...
}
可以在定义 defaultState 时,为 age 提供非法值,可以看到非法值在 initAction 中被处理掉了。
1.5 在 Father 组件中点击按钮修改 name 的值
注意:禁止直接修改state数据源。
注意:这种用法是错误的,因为不能直接修改 state 的值
因为存储在 useReducer 中的数据都是“不可变”的!
要想修改 useReducer 中的数据,必须触发 reducer 函数的重新计算,
根据 reducer 形参中的旧状态对象(initState),经过一系列处理,返回一个“全新的”状态对象
state.name = ‘escook’
1.5.1为了能够触发 reducer 函数的重新执行,我们需要在调用 useReducer() 后接收返回的 dispatch 函数
// Father 父组件
const [state, dispatch] = useReducer(reducer, defaultState, initAction)
在 button 按钮的点击事件处理函数中,调用 dispatch() 函数,从而触发 reducer 函数的重新计算:
// Father 父组件
const onChangeName = () => {
dispatch()
}
点击 Father 组件中如下的 button 按钮:
<button onClick={onChangeName}>修改 name 的值</button>
会触发 reducer 函数的重新执行,并打印 reducer 中的 console.log(),代码如下:
const reducer = (prevState: UserType) => {
console.log('触发了 reducer 函数')
return prevState
}
1.5.2 接收并处理dispatch参数
// 1. 定义 action 的类型
type ActionType = { type: 'UPDATE_NAME'; payload: string }
// 2. 为 action 指定类型为 ActionType
const reducer = (prevState: UserType, action: ActionType) => {
console.log('触发了 reducer 函数', action)
// 3. 删掉之前的代码,再重复编写这段逻辑的时候,会出现 TS 的类型提示,非常 Nice
switch (action.type) {
case 'UPDATE_NAME':
return { ...prevState, name: action.payload }
default:
return prevState
}
}
//同时,在 Father 组件的 onChangeName 处理函数内,调用 dispatch() 时也有了类型提示:
const onChangeName = () => {
dispatch({ type: 'UPDATE_NAME', payload: '测试' })
}
注意:在今后的开发中,正确的顺序是先定义 ActionType 的类型,再修改 reducer 中的 switch…case… 逻辑,最后在组件中调用 dispatch() 函数哦!这样能够充分利用 TS 的类型提示。
1.6 使用 Immer 编写更简洁的 reducer 更新逻辑
1.6.1 安装Immer
npm install immer use-immer -S
1.6.2 从 use-immer 中导入 useImmerReducer
// 1. 导入 useImmerReducer
import { useImmerReducer } from 'use-immer'
// 父组件
export const Father: React.FC = () => {
// 2. 把 useReducer() 的调用替换成 useImmerReducer()
const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)
}
1.6.3 case代码块汇总不再需要return
Immer 内部会复制并返回新对象
const reducer = (prevState: UserType, action: ActionType) => {
console.log('触发了 reducer 函数', action)
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
default:
return prevState
}
}
二.useContext
2.1 语法格式(使用步骤)
- 在全局创建 Context 对象
- 在父组件中使用 Context.Provider 提供数据
- 在子组件中使用 useContext 使用数据
import React, { useContext } from 'react'
// 全局
const MyContext = React.createContext(初始数据)
// 父组件
const Father = () => {
return <MyContext.Provider value={{name: 'escook', age: 22}}>
<!-- 省略其它代码 -->
</MyContext.Provider>
}
// 子组件
const Son = () => {
const myCtx = useContext(MyContext)
return <div>
<p>姓名:{myCtx.name}</p>
<p>年龄:{MyCtx.age}</p>
</div>
}
2.2 createContext 配合 useContext 使用
import React, { useState, useContext } from 'react'
// 声明 TS 类型
type ContextType = { count: number; setCount: React.Dispatch<React.SetStateAction<number>> }
// 1. 创建 Context 对象
const AppContext = React.createContext<ContextType>({} as ContextType)
export const LevelA: React.FC = () => {
const [count, setCount] = useState(0)
return (
<div style={{ padding: 30, backgroundColor: 'lightblue', width: '50vw' }}>
<p>count值是:{count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>+1</button>
{/* 2. 使用 Context.Provider 向下传递数据 */}
<AppContext.Provider value={{ count, setCount }}>
<LevelB />
</AppContext.Provider>
</div>
)
}
export const LevelB: React.FC = () => {
return (
<div style={{ padding: 30, backgroundColor: 'lightgreen' }}>
<LevelC />
</div>
)
}
export const LevelC: React.FC = () => {
// 3. 使用 useContext 接收数据
const ctx = useContext(AppContext)
return (
<div style={{ padding: 30, backgroundColor: 'lightsalmon' }}>
{/* 4. 使用 ctx 中的数据和方法 */}
<p>count值是:{ctx.count}</p>
<button onClick={() => ctx.setCount((prev) => prev + 1)}>+1</button>
<button onClick={() => ctx.setCount(0)}>重置</button>
</div>
)
}
2.3 以非侵入的方式使用 Context【封装Context】
浸入的方式就是父组件中存在MyContext.Provider代码。
核心思路:每个 Context 都创建一个对应的 Wrapper 组件,在 Wrapper 组件中使用 Provider 向
children 注入数据。
注意:Wrapper组件的命名:是由Context的名称+Wrapper构成。
// 声明 TS 类型
type ContextType = { count: number; setCount: React.Dispatch<React.SetStateAction<number>> }
// 创建 Context 对象
const AppContext = React.createContext<ContextType>({} as ContextType)
// 定义独立的 Wrapper 组件,被 Wrapper 嵌套的子组件会被 Provider 注入数据
export const AppContextWrapper: React.FC<React.PropsWithChildren> = (props) => {
// 1. 定义要共享的数据
const [count, setCount] = useState(0)
// 2. 使用 AppContext.Provider 向下共享数据
return <AppContext.Provider value={{ count, setCount }}>{props.children}</AppContext.Provider>
}
App.tsx
import React from 'react'
import { AppContextWrapper, LevelA } from '@/components/use_context/01.base.tsx'
const App: React.FC = () => {
return (
<AppContextWrapper>
<!-- AppContextWrapper 中嵌套使用了 LevelA 组件,形成了父子关系 -->
<!-- LevelA 组件会被当做 children 渲染到 Wrapper 预留的插槽中 -->
<LevelA />
</AppContextWrapper>
)
}
export default App