useContext
在 react 函数式组件中,如果组件的嵌套层级很深,当父组件想把数据共享给最深层的子组件时,传统的办法是使用 props,一层一层把数据向下传递。
使用 props 层层传递数据的维护性太差了,我们可以使用 React.createContext() + useContext()
轻松实现多层组件的数据传递。
1. useContext 的语法格式
主要的使用步骤如下:
1. 在全局借助 React.createContext 创建 Context 对象
2. 在顶层组件中使用 Context.Provider + value 提供数据
3. 在接收组件中使用 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. 定义组件结构
定义 LevelA,LevelB,LevelC 的组件结构如下:
import React, { useState } from 'react'
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>
{/* 使用子组件 */}
<LevelB />
</div>
)
}
export const LevelB: React.FC = () => {
return (
<div style={{ padding: 30, backgroundColor: 'lightgreen' }}>
{/* 使用子组件 */}
<LevelC />
</div>
)
}
export const LevelC: React.FC = () => {
return (
<div style={{ padding: 30, backgroundColor: 'lightsalmon' }}>
<button>+1</button>
<button>重置</button>
</div>
)
}
3. createContext 配合 useContext 使用
1、在父组件中,调用 React.createContext
向下共享数据
2、在子组件中调用 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>
)
}
4. ☆☆☆以【非侵入的方式】使用 Context
在刚才的案例中,我们发现父组件 LevelA 为了向下传递共享的数据,在代码中侵入了 <AppContext.Provider>
这样的代码结构。
为了保证父组件中代码的单一性,也为了提高 Provider 的通用性,我们可以考虑把 Context.Provider 封装到独立的 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>
}
定义好 Wrapper 组件后,我们可以在 App.tsx 中导入并使用 Wrapper 和 LevelA 组件,代码如下:
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
这样,组件树的嵌套关系为:App => Wrapper => LevelA => LevelB => LevelC。因此在 LevelA、LevelB 和 LevelC 组件中,都可以使用 context 中的数据。例如,LevelA 组件中的代码如下:
export const LevelA: React.FC = () => {
// 使用 useContext 接收数据
const ctx = useContext(AppContext)
return (
<div style={{ padding: 30, backgroundColor: 'lightblue', width: '50vw' }}>
{/* 使用 ctx 中的数据和方法 */}
<p>count值是:{ctx.count}</p>
<button onClick={() => ctx.setCount((prev) => prev + 1)}>+1</button>
<LevelB />
</div>
)
}
LevelC 组件中的代码如下:
export const LevelC: React.FC = () => {
// 使用 useContext 接收数据
const ctx = useContext(AppContext)
return (
<div style={{ padding: 30, backgroundColor: 'lightsalmon' }}>
{/* 使用 ctx 中的数据和方法 */}
<p>count值是:{ctx.count}</p>
<button onClick={() => ctx.setCount((prev) => prev + 1)}>+1</button>
<button onClick={() => ctx.setCount(0)}>重置</button>
</div>
)
}
核心思路:
1、每个 Context 都创建一个对应的 Wrapper 组件,在 Wrapper 组件中使用 Provider 向 children 注入数据
2、实际上就是为了避免不必要的代码污染父组件,请来了一个顶层组件 Wrapper,并将需要传递的状态以及 provider 放在该组件中,后续使用流程不变
3、最大的优点是通用性
5. 使用 useContext 重构 useReducer 案例
1、定义 Context 要向下共享的数据的 TS 类型,代码如下:
// 1. 定义 Context 的 TS 类型
// 在这一步,我们必须先明确要向子组件注入的数据都有哪些
type UserInfoContextType = { user: UserType; dispatch: React.Dispatch<ActionType> }
2、使用 React.createContext 创建 Context 对象:
// 2. 创建 Context 对象
const UserInfoContext = React.createContext<UserInfoContextType>({} as UserInfoContextType)
3、创建 ContextWrapper 组件如下,把 Father 组件中的 useImmerReducer 调用过程,抽离到 ContextWrapper 中:
// 3. 创建 ContextWrapper 组件
export const UserInfoContextWrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)
return <UserInfoContext.Provider value={{ user: state, dispatch }}>{children}</UserInfoContext.Provider>
}
4、改造 Father 组件,调用 useContext 获取并使用 Context 中的数据。同时,Father 组件也不必再使用 props 把 state 和 dispatch 函数传递给 Son 组件:
export const Father: React.FC = () => {
// 4. 调用 useContext 导入需要的数据
const { user: state, dispatch } = useContext(UserInfoContext)
const changeUserName = () => dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })
return (
<div>
<button onClick={changeUserName}>修改用户名</button>
<p>{JSON.stringify(state)}</p>
<div className="father">
{/* 5. 这里没有必要再往子组件传递 props 了 */}
{/* <Son1 {...state} dispatch={dispatch} />
<Son2 {...state} dispatch={dispatch} /> */}
<Son1 />
<Son2 />
</div>
</div>
)
}
5、改造 App 根组件,分别导入 UserInfoContextWrapper 和 Father 组件,并形成父子关系的嵌套,这样 Father 及其子组件才可以访问到 Context 中的数据:
import React from 'react'
import { UserInfoContextWrapper, Father } from '@/components/use_reducer/01.base.tsx'
const App: React.FC = () => {
return (
<UserInfoContextWrapper>
<Father />
</UserInfoContextWrapper>
)
}
export default App
6、最后,改造 Son1,Son2 和 GrandSon 组件,删除 props 及其类型定义,改用 useContext() 来获取 UserInfoContextWrapper 向下注入的数据:
const Son1: React.FC = () => {
// 6. 把 props 替换为 useContext() 的调用
const { dispatch, user } = useContext(UserInfoContext)
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 = () => {
// 7. 把 props 替换为 useContext() 的调用
const { dispatch, user } = useContext(UserInfoContext)
const sub = () => dispatch({ type: 'DECREMENT', payload: 5 })
return (
<div className="son2">
<p>{JSON.stringify(user)}</p>
<button onClick={sub}>年龄-5</button>
<hr />
<GrandSon />
</div>
)
}
const GrandSon: React.FC = () => {
// 8. 把 props 替换为 useContext() 的调用
const { dispatch } = useContext(UserInfoContext)
const reset = () => dispatch({ type: 'RESET' })
return (
<>
<h3>这是 GrandSon 组件</h3>
<button onClick={reset}>重置</button>
</>
)
}