在 React 中,props 能够帮助我们将数据层层往下传递。而 context 能够帮助我们将数据跨层级往下传递。
context 的概念稍微有一点点多,但是我们在学习的他的时候,只需要将其分为两个部分,就能够轻松掌握
- 1、如何创建 context 与如何传递数据
- 2、组件中如何获取数据
context 如何创建与数据如何传递
react 中使用 createContext 在组件外部创建 context
const context = createContext(defaultValue)
context 本身不保存任何信息,他包含了两个引用
context.Provider 用于包裹子组件并传递数据
context.Consumer 用于在子组件中读取数据,不过这个读取方式已经非常少能有用武之地了,基本上都被 useContext 取代了。
一个非常简单的 demo 如下。首先我们一定要明确的把 Provider 当成顶层父组件,因为我们的目标就是把数据从父组件往更低层的子组件传递,因此我们首先要创建父组件
import { createContext } from 'react'; const ThemeContext = createContext('light'); function App() { const [theme, setTheme] = useState('light'); // ... return ( <ThemeContext.Provider value={theme}> <Page /> </ThemeContext.Provider> ); }
Provider 通过 value 将定义好的数据传递下去。在子组件 Page 以及他更低层的子组件中,我们都可以使用 useContext 来获取数据
数据如何获取
假如在上面案例的子组件 Page 内部,还有一个更底层次的子组件 Button , 在 Button 中,我们可以通过 useContext 这个 hook 来获取从顶层父组件传递过来的参数
function Button() { // ✅ Recommended way const theme = useContext(ThemeContext); return <button className={theme} />; }
当然,在以前我们也可以通过 Consumer 来获取,不过现在已经不推荐这样使用了
function Button() { // 🟡 Legacy way (not recommended) return ( <ThemeContext.Consumer> {theme => ( <button className={theme} /> )} </ThemeContext.Consumer> ); }
支持嵌套
多个 context 可以嵌套使用
import { createContext } from 'react'; const ThemeContext = createContext('light'); const AuthContext = createContext(null); function App() { const [theme, setTheme] = useState('dark'); const [currentUser, setCurrentUser] = useState({ name: 'Taylor' }); // ... return ( <ThemeContext.Provider value={theme}> <AuthContext.Provider value={currentUser}> <Page /> </AuthContext.Provider> </ThemeContext.Provider> ); }
结合 TS 使用
我们要结合 TS 来实现一个案例,在子组件中有两个按钮,他们分别可以对数字进行递增或者递减操作。
首先我们简单调整一下实现思路,封装一个顶层父组件,并在该父组件中约定好数据和操作数据的方法。接收子组件为参数
先使用 interface 约定好数据的类型
interface Injected { counter: number, setCounter: Dispatch<any>, increment: () => any, decrement: () => any }
顺带简单定义一下 props 的类型,目前只接收一个 children 作为参数
interface Props { children?: any }
然后创建的 context,createContext 接收刚才约定好的类型作为泛型传入
export const context = createContext<Injected>({} as Injected)
准备工作做好了之后,接下来约定好数据即可,组件代码如下
export function CounterProvider({children}: Props) { const [counter, setCounter] = useState(0) const value = { counter, setCounter, increment: () => setCounter(counter + 1), decrement: () => setCounter(counter - 1) } return ( <context.Provider value={value}> {children} </context.Provider> ) }
顶层父组件封装好之后,我们只需要将子组件封装好,然后组合起来即可
export default () => ( <CounterProvider> <Demo /> </CounterProvider> )
在子组件中,使用 useContext 获取数据和操作数据的方法
import {useContext} from 'react' import Button from 'src/components/Button' import {context, CounterProvider} from './CounterProvider' function Demo() { const {counter, increment, decrement} = useContext(context) return ( <div> <div>{counter}</div> <Button onClick={increment}>递增</Button> <Button onClick={decrement}>递减</Button> </div> ) }
改造:结合 useReducer 来使用
一些团队或者开源项目,会基于 context 和 useReducer 来封装状态管理,用来替代 redux 在项目中的地位。这是一个非常不错的想法。现在我们把上面一个案例稍微改造一下,也来试试。
案例目录结构如下,index.tsx 为项目的入口,Counter 表示子组件,Provider 表示顶层父组件
+ App - index.tsx - Provider.tsx - Counter.tsx
假如项目的子组件和顶层父组件都已经封装好了,那么在入口文件中的代表应该为
import {Provider} from './Provider' import Counter from './Counter' export default function App() { return ( <Provider> <Counter /> </Provider> ) }
我们接下来先思考一下顶层的 Provider 组件应该如何封装。
首先,我们需要先约定好 state 的类型,该案例中,只有一个数字,因此类型定义为
interface State { counter: number }
context 要往底层组件中传递修改数据的方式,因此还需要定义另外一个 context 的类型
interface Injected extends State { increment: () => any, decrement: () => any }
然后做一些其他的简单类型约定
interface Props { children?: any } type Action = { type: string, [key: string]: any }
定义好初始状态
const initialState = { counter: 0 }
定义 context
export const context = createContext<Injected>(initialState as Injected)
定义好 reducer,这里需要特别注意的是 reducer 的类型一定要约定好
const reducer: Reducer<State, Action> = (state, action) => { if (action.type == 'increment') { return { counter: state.counter + 1 } } if (action.type == 'decrement') { return { counter: state.counter - 1 } } return state }
准备工作都做好了之后,我们再定义 Provider 组件
export function Provider({children}: Props) { const [state, dispatch] = useReducer(reducer, initialState) const value = { counter: state.counter, increment: () => dispatch({ type: 'increment' }), decrement: () => dispatch({ type: 'decrement' }), } return ( <context.Provider value={value}> {children} </context.Provider> ) }
这样,顶层父组件就搞定了。剩下的就是封装子组件。子组件只要包裹在我们封装好的 Provider 之下,我们就可以在子组件中通过 useContext 轻松获取状态,代码如下
import {useContext} from 'react' import Button from 'src/components/Button' import {context} from './Provider' export default function Counter() { const {counter, increment, decrement} = useContext(context) return ( <div> <div>{counter}</div> <Button onClick={increment}>递增</Button> <Button onClick={decrement}>递减</Button> </div> ) }
大功告成。惊喜的是,在逻辑清晰的情况下,我们发现 useReducer + useContext 使用起来也不是很困难。
我们在来一个更复杂一点的案例,巩固一下我们学习到的知识。
更复杂的案例
需求是实现一个任务列表。
- 1、 列表中的每一项都可以被删除
- 2、 列表中的每一项都可以编辑
- 3、 可以新增列表
思考一下之后,我决定把列表单独封装在一个子组件里,新增列表的操作封装在另外一个子组件里,然后使用 Provider 把他们包裹起来,项目的结果如下
+ App - index.tsx - Provider.tsx - TaskList.tsx - AddTask.tsx
在封装 Provider 时,我们可以把在内部基于 useContext 封装一些自定义 hooks,来简化子组件的操作
export function useTasks() { return useContext(TasksContext) } export function useDispatch() { return useContext(DispatchContext) }
先约定好一些前置的类型声明
export type Task = { id: number, text: string, done: boolean } interface Props { children?: any } export type Action = { type: string, [key: string]: any }
约定初始化数据
const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
定义两个 context。分别用于传递数据和操作数据的方法。这里只是为了增加语法说明新增的操作方式,实践中不必非要如此
export const TasksContext = createContext(initialTasks) const DispatchContext = createContext<Dispatch<Action>>(null as any)
定义好 reducer 函数
const reducer: Reducer<Task[], Action> = (tasks, action) => { if (action.type == 'added') { return [ ...tasks, { id: action.id, text: action.text, done: false } ] } if (action.type == 'changed') { return tasks.map(t => { if (t.id == action.task.id) { return action.task } return t }) } if (action.type == 'deleted') { return tasks.filter(t => t.id !== action.id) } return tasks }
最后定义 Provider
export function Provider({children}: Props) { const [tasks, dispatch] = useReducer(reducer, initialTasks) return ( <TasksContext.Provider value={tasks}> <DispatchContext.Provider value={dispatch}> {children} </DispatchContext.Provider> </TasksContext.Provider> ) } export function useTasks() { return useContext(TasksContext) } export function useDispatch() { return useContext(DispatchContext) }
子组件的逻辑就比较简单了,只需要通过自定义的 useTasks 和 useDispatch 获取数据和对应的操作即可。
// AddTask.tsx import { useState } from 'react'; import { useDispatch } from './Provider'; export default function AddTask() { const [text, setText] = useState(''); const dispatch = useDispatch(); return ( <> <input placeholder="Add task" value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => { setText(''); dispatch({ type: 'added', id: nextId++, text: text, }); }}>Add</button> </> ); } let nextId = 3;
// TaskList.tsx import { useState } from 'react'; import { useTasks, useDispatch, Task } from './Provider'; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map(task => ( <li key={task.id}> <TaskItem task={task} /> </li> ))} </ul> ); } function TaskItem({ task }: { task: Task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
子组件和顶层父组件都封装好之后,我们只需要在 App.tsx 中把他们组合起来就可以了
import AddTask from './AddTask'; import TaskList from './TaskList'; import { Provider } from './Provider'; export default function TaskApp() { return ( <Provider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </Provider> ); }
OK,搞定。虽然这个例子从交互上变得更加复杂了,但是理解起来的难度并没有任何增加。基于这套逻辑,稍微扩展丰富一下,你就能开发出来一个自己的状态管理器。
不过,也别高兴得太早,关于 context,还有一些东西需要我们去攻克,他跟性能优化有关,我们在后续的文章中,继续学习。