cover

使用 Context 避免深层属性传递

前言

最近在项目发现了一些看起来很神奇的代码,一个组件 A 的方法通过 props 传到组件 B,然后到组件 C,再到组件 D,再到组件 E,最后到组件 D, 简直是千层饼😂。

Xnip2021-07-19_20-03-31.jpg

提问一番后知道了这种将属性深层传递的现象叫 Prop Drilling,本文说明如何使用 React 的 Context API 避免这种现象。

什么是 Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但此种用法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。这些组件共享的以及变化不频繁的数据正是 Context 用武之地。

如何使用 Context

举个例子🌰 说明
react-context-hooks.vercel.app_(iPhone 6_7_8).png
在线示例
本文代码

创建 Context

首先用 React.createContext 方法创建一个 AuthContext,表示当前应用的认证状态,创建时可以传入默认值。

1
2
3
4
5
6
// AuthContext.js

export const AuthContext = React.createContext({
user: null,
isAuthenticated: false,
})

然后创建 AuthContextProvider 组件,在组件中初始化状态,定义改变状态的方法,将这些数据和方法传递给 Context.Providervalue 属性 ,value 可以接收数据和方法,最后在组件中渲染子组件 children

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// AuthContext.js

export class AuthContextProvider extends Component {
state = {
user: null,
isAuthenticated: false,
}

login = (user) => {
// ...
}

logout = () => {
// ...
}

render() {
return (
<AuthContext.Provider value={(this.state, this.login, this.logout)}>
{this.props.children}
</AuthContext.Provider>
)
}
}

使用 Context

有多种方式获得 Context 中的数据

  1. 使用 Context.Comsumer
1
2
3
4
5
6
7
8
9
// AppHeader.js

<AuthContext.Consumer>
{(authContext) => (
<header>
// ...
</header>
)}
</AuthContext.Consumer>
  1. 使用组件的 ContextType 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AppFooterButtons.js

class AppFooterButtons extends Component {
static contextType = AuthContext

render() {
return (
<div>
<button onClick={this.login}>Login</button>
<button onClick={this.context.logout}>Logout</button>
</div>
)
}
}
  1. 使用 useContext

对于函数式组件可以使用 useContext 方法获取状态数据,可以同时获取多个状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AppBody.js

const AppBoy = () => {
const authContext = useContext(AuthContext)
const { languages, current, changeLanguage } = useContext(LanguageContext)

return (
<div>
<h1>
Hello, {authContext.isAuthenticated ? authContext.user.name : 'World'}
</h1>
</div>
)
}

修改 Context

  1. 对于 class 组件,使用 setState 方法更新组件的 state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// AuthContext.js 

export class AuthContextProvider extends Component {
// ...

login = (user) => {
if (this.state.isAuthenticated) {
return
}
this.setState({
user,
isAuthenticated: true,
})
}

logout = () => {
this.setState({
user: null,
isAuthenticated: false,
})
}

// ...
}
  1. 函数式组件使用 useState
1
2
3
4
5
6
7
8
9
10
11
12
// LanguageContext.js

export const LanguageContextProvider = (props) => {
// ...

const [current, setCurrent] = useState(0)
const changeLanguage = (val) => {
setCurrent(val)
}

// ...
}

最终使用 Context 将应用中通用状态统一管理,无需再层层传递属性。

结论

本文说明什么是 Context,以及使用 Context 的多种方式。使用 Context 可以有效的避免 Prop Drilling 现象,将需要深层传递的属性和方法提取出来,达到共用的目的。应用 Context 的最佳时机是应用的一些全局性,不会频繁变化的数据。

注意事项

使用 Context 之前的考虑

Context 主要应用场景在于_很多_不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

参考

Avoid Prop Drilling with React Context
Prop Drilling
Context

Buy Me A Coffee
← 关于新拟物设计 Neumorphism 学习抽象语法树 AST →