Context
# Context
按照之前的内容,如果在一个组件里想向子组件传递一些数据,那么可以使用 props,但是如果要连续多级传递数据那么就要连续传递 props,这回导致代码冗长,Context 允许父组件向下层的任何深度的任何组件传递信息,而无需 props 显式传递。
# 使用 Context
现在以一个传递 memberId 的场景为例,多级传递 Context。
# 创建 Context
export const MemberIdContext = createContext<string>('');
# 使用 Context
创建 Context 之后,使用 useContext
使用 Context:
// Son.tsx
import {FC, useContext} from "react";
import {GrandSon, MemberIdContext} from "./index";
const Son: FC = () => {
const memberId = useContext(MemberIdContext);
return (
<>
<h2>{memberId}</h2>
<GrandSon/>
</>
)
}
export {Son}
// GrandSon.tsx
import {FC, Fragment, useContext} from "react";
import {MemberIdContext} from "./index";
const GrandSon: FC = () => {
const memberId = useContext(MemberIdContext);
return (
<Fragment>
<h3>{memberId}</h3>
</Fragment>
)
}
export {GrandSon}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
除了通过 useContext
使用 Context 之外,也可以使用 <Consumer>/<Consumer>
标签将要使用 Context 的标签包裹起来:
import {FC} from "react";
import {GrandSon, MemberIdContext} from "./index";
const Son: FC = () => {
return (
<>
<MemberIdContext.Consumer>
{
value => <h2>{value}</h2>
}
</MemberIdContext.Consumer>
<GrandSon/>
</>
)
}
export {Son}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Tips
老版本 React 函数式组件可以通过第二个入参接收 Context,但是新版本建议使用 Hooks 或者 Consumer 标签。
# 提供 Context
在最外层的组件中使用 <Provider></Provider>
标签将希望透传 Context 的标签包裹起来,这样在子组件中使用 Context 时,React 会将距离子组件最近的 <Provider></Provider>
标签传递的值传给子组件:
// App.tsx
import {FC, useEffect, useState} from "react";
import {MemberIdContext, Son} from "./components";
const MyApp: FC = () => {
const [memberId, setMemberId] = useState('');
return (
<>
<MemberIdContext.Provider value={memberId}>
<Son/>
</MemberIdContext.Provider>
<button onClick={() => {setMemberId(memberId + '1')}}>click</button>
</>
)
}
export {MyApp}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Context 的注意点
在使用 Context 之前先考虑下面几种替代方案:
- 从传递 props 开始:如果组件不是很复杂,可以通过 props 向下传递,这可以让数据的流动更加清晰。
- 抽象组件并将 JSX 作为 children prop 传递。如果要将 props 经过多个不使用它的组件向下传递,那么可以在这些中间组件中接收 props.children 并在对应位置渲染,然后直接使用这些中间组件包裹要使用 props 的组件。
# Context 的使用场景
- 主题:例如修改外观的黑暗模式。
- 当前账户:例如可以通过 Context 传递当前登录的用户的信息。
- 路由。
- 状态管理。
# 同时使用 Reducer 和 Context
现在回到 Reducer 中的待办事项的例子,dispatch 方法仅在顶级组件 App 中可用,要让 TodoList 组件中可以调用那么必须要通过 props 显式传递状态和事件处理器。如果有很多组件、很多事件处理器和状态,那么传递所有的状态和事件处理器会非常麻烦,如果将状态和 dispatch 都通过 Context 传递,那么所有的子组件都能获取到状态和 dispatch 函数。下面改造此前的 Reducer 中的 TodoList 的例子。
第一步仍然是创建 Context:
export interface action extends Todo {
type: 'add' | 'change' | 'delete';
}
type TodosReducer = {
todos?: Array<Todo>;
dispatch?: Dispatch<action>;
}
export const TodosContext = createContext<TodosReducer>({});
2
3
4
5
6
7
8
9
10
11
然后在 TodoList 中改用 Context:
const TodoList: FC = () => {
const [editingTodo, setEditingTodo] = useState<Todo>(() => ({id: '', content: ''}));
const ctx = useContext(TodosContext);
const todos = ctx.todos ? ctx.todos : [];
const dispatch = ctx.dispatch;
const isEditing = (todo: Todo) => editingTodo.id === todo.id;
return (
<Fragment>
<ul>
{
todos ? todos.map((todo) => {
return (
<li key={todo.id}>
{
isEditing(todo) ?
<input
value={editingTodo.content}
onChange={(e) => {
setEditingTodo({...editingTodo, content: e.target.value})
}}
/> :
todo.content
}
<button
onClick={
() => {
if (isEditing(todo) && dispatch) {
dispatch({
type: 'change',
...editingTodo,
})
setEditingTodo({id: '', content: ''});
return
}
setEditingTodo(todo)
}}>
{isEditing(todo) ? 'save' : 'edit'}
</button>
<button onClick={() => {
if (dispatch) {
dispatch({
type: 'delete',
id: todo.id,
content: ''
})
}
}}>delete
</button>
</li>
)
}) : undefined
}
</ul>
</Fragment>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
最后从父组件中传入 Context:
const App: FC = () => {
const [todos, dispatch] = useReducer<Reducer<Array<Todo>, action>>(tasksReducer, []);
const [value, setValue] = useState('');
const addHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
dispatch({
type: 'add',
id: '',
content: value
})
setValue('');
}
}
return (
<TodosContext.Provider value={{todos, dispatch}}>
<input
value={value}
onChange={(e) => {setValue(e.target.value)}}
onKeyDown={addHandler}
/>
<TodoList/>
</TodosContext.Provider>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24