「React 知命境」第 28 篇
在 React 中,props 能够帮助我们将数据层层往下传递。而 context 能够帮助我们将数据跨层级往下传递。
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 来实现一个案例,在子组件中有两个按钮,他们分别可以对数字进行递增或者递减操作。首先我们简单调整一下实现思路,封装一个顶层父组件,并在该父组件中约定好数据和操作数据的方法。接收子组件为参数
先使用 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>
)
}
一些团队或者开源项目,会基于 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
使用起来也不是很困难。
我们在来一个更复杂一点的案例,巩固一下我们学习到的知识。
需求是实现一个任务列表。
思考一下之后,我决定把列表单独封装在一个子组件里,新增列表的操作封装在另外一个子组件里,然后使用 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,还有一些东西需要我们去攻克,他跟性能优化有关,我们在后续的文章中,继续学习。