前言
Context提供了一个无需为每层组件手动添加props,就能在组件树间进行数据传递的方法。Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据。使用context,我们可以避免通过中间元素传递props
context API
React.createContextContext.ProviderClass.contextTypeContext.ConsumerContext.displayName
何时使用Context
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
使用Context之前的考虑
Context主要应用场景在于很多不同层级的组件需要访问同样的一些数据。请谨慎使用,因为这会使得组价的复用性变差。
如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比context更好的解决方案。
1. Context(上下文)要解决什么问题?
解决组件间通信的问题,使用context来保存全局状态。有些全局的状态,需要多个组件使用。如果都通过一层层传递的方式来做,会非常麻烦,书写大量的...props会导致逻辑异常的混乱。相信你也曾经历过在一个 层级非常深的组件中获取最外层组件的state,一旦中间某个流程有疏漏,那你的页面就GG了...。这时,我们的Context API就英勇上场了。我们常用的Redux、react-router等第三方库中也重度依赖了Context API.
2. Context的使用场景用哪些?
如上图所示,左边很多个组件需要共享一个全局的上下文数据。作为根节点,用来提供共享的全局上下文数据,从而很方便的共享全局状态。子节点则可以通过Context API来访问这个全局数据,无论是在那一层,无需上一层传递props,子节点可以自己访问。因此,根节点称为provide,子节点称为consume.
代码示例:
const ThemeContext = React.createContext('light')
class App extends React.Component {
redner () {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
)
}
}
// 层级上属于Provider的子组件Consumer
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme}/>}
</ThemeContext.Consumer>
)
}
如何使用 DEMO:
import React from 'react'
const enStrings = {
submit: 'submit',
cancel: 'cancel',
}
const cnStrings = {
submit: '提交',
cancel: '取消',
}
// 设置默认值
// 1. 使用 createContext 创建上下文
const LocaleContext = React.createContext(enStrings)
// 2. 创建 Provider
class LocaleProvider extend React.Component {
// 默认状态
state = { locale: cnStrings }
// 切换语言
toggleLocale = () => {
const locale = this.state.locale === enStrings ? cnStrings : enStrings
this.setState({ locale })
}
render() {
return (
// state变化,则LocaleContext的value将方法变化,下面使用它的地方将全部变化
// 在 Provider 中将 state 通过 value 提供出去(根节点)
<LocaleContext.Provider value={this.state.locale}>
<button onClick={this.toggleLocale}>
切换语言
</button>
// 将childen显示在此处
{this.props.children}
</LocaleContext.Provider>
)
}
}
// 3. 创建 Consumer
class LocaledButtons extends React.Component {
render () {
return (
<LocaleContext.Consumer>
// 函数作为子组件
{locale => (
<div>
<button>{locale.cancel}</button>
<button>{locale.submit}</button>
</div>
)}
</LocaleContext.Consumer>
)
}
}
// 开始使用,不用主动监听变化,react主动监听,并通知子组件自动刷新
export default () => {
<div>
<LocalePorvider>
// children
<div>
<LocaledButtons />
</div>
</LocalePorvider>
</div>
}
-
- 创建
LocaleContext
- 创建
-
- 根据
LocaleContext创建LocaleContext.Provider
- 根据
-
- 根据
LocaleContext创建LocaleContext.Consumer
- 根据
深度使用...
在目前常见的项目如React或TS项目中,我们经常会封装一个service层来进行ajax请求的做法。
<!--ServiceContainer.tsx-->
export class serviceContainer extends Component {
// 获取接口数据
getService(config, callback) {
// 订阅 ajax 可观察对象,触发其观察数据流(否则不会有任何请求)
const subscriber = this.getApiData(config, callback).subscribe()
// 添加一个 tear down 以便在 subscription 的 unsubscribe() 期间调用
this.subscription.add(subscriber)
}
// 获取接口数据
getApiData(config, callback) {
this.props.setAjaxStatus('loading') // 标记加载中
const _config = Object.assign(this.baseAjaxConfig, this.props.ajaxConfig, config)
// 返回一个 ajax 可观察对象
// 将 res 传递到外部 adapter() 中进一步格式化处理后返回数据
return this.fetchData(_config).pipe(
map( (res: any) => this.adapterData(res, callback) )
)
}
fetchData (ajaxOptions: AjaxRequest) {
}
componentDidMount () {
if (!!this.props.isServiceAutoFetch) {
this.getService()
}
}
componentWillUnmount() {
// 清理 subscription 持有的资源
this.subscription.unsubscribe()
}
render () {
const { Provider } = this.props.context
const { value } = this.props
// 导出方法函数
value.getService = this.getService
value.getObservable = this.getObservable
value.resetService = this.resetService
return (
<Provider value={value}>
{this.props.children}
</Provider>
)
}
}
// setContext.tsx
import * as React from 'react'
import { Context, ReactNode } from 'react'
export const setContext = (propName: string, Context: Context<any>) => (Orig: any): any => (props: any): ReactNode => {
return (
<Context.Consumer>
// 还是以函数作为子组件的方式使用
{(value: any) => <Orig {...props} {...{ [propName]: value }} />}
</Context.Consumer>
)
}
// 接口的service
import { createContext } from 'react'
import { ServiceContainer } from '...'
// 初始值
const defaultValue = {
}
// 创建 Context
const context = createContext(defaultValue)
// 服务主体 (xxx代表接口名称)
function xxxService (props) {
...
return (
<ServiceContainer
context={context}
setAjaxStatus={setAjaxStatus}
setErrorMsg={setErrorMsg}
resetData={resetData}
adapter={adapter}
value={state}
nationConfig={nationConfig}
isServiceAutoFetch={isServiceAutoFetch !== false}
ajaxConfig={ajaxConfig}
>
{children}
</ServiceContainer>
)
}
// 最终导出
const xxxContext = context
export {
xxxService,
xxxrContext,
}
// 使用
import { xxxContext } from '...' // 感觉只是引入了默认值,有什么用...,调用ajax还是在config文件的路由下处理的,因此service与路由有强相关...
useContext
useContext相当于<MyContext.Consumer>或class组件中的static contextType = MyContext
useContext(My)只是让你能够读取context的值以及订阅context的变化。你仍然需要在上层组件树中使用<MyContext.Provider>来为下层组件提供context。
const value = useContext(MyContext)
接收一个context对象(React.createContext的返回值)并返回该context的当前值。当前的context值由上层组件中距离当前组件最近的<MyContext.Provider>的value prop决定。
当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重新渲染,并使用最新传递给MyContext Provider的context value值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
useContext的参数必须是context对象本身:
- 正确: useContext(MyContext)
- 错误: useContext(MyContext.Consumer)
- 错误: useContext(MyContext.Provider)
调用了useContext的组件总会在context值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用memoization 来优化 。
把如下代码与Context.Provider放在一起使用
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee'
},
dark: {
foreground: '#ffffff',
background: '#222222'
}
}
const ThemesContext = React.createContext(themes.light)
function App () {
return (
<ThemesContext.Provider value={themes.dark}>
<toolbar />
</ThemesContext.Provider>
)
}
function Toolbar(props) {
return (
<div>
<ThemesButton />
</div>
)
}
function ThemesButton() {
const theme = useContext(ThemeContext)
return (
<button style={{ background: theme.background, color: theme.foreground}}>
I am styled by theme context!
</button>
)
}
总结:
useContext相当于<MyContext.Consumer>来使用,使用方法如上面的例子。useContext只能让你能够读取context的值以及订阅context的变化。需要在上层组件树中使用<MyContext.Provider>来为下层组件提供context。
综上,可以看到,useContext解决了组件间多层传递props状态值的问题,但是useContext本身并不能改变state的值。而useReducer却可以解决复杂状态下状态值state的管理。因此,可以将全局状态值放入useReducer中,将返回的state和dispatch通过<MyContext.Provider>进行传值,这样在子组件中就可以用useContext来接收相应的状态值,并通过dispatch对状态值进行操作。这样,就可以将useContext和useReducer相结合实现Redux的功能!
后记
关于useReducer+useContext如果使用来替代redux,可以参考这篇文章:[一文看懂] useReducer + useContext 如何使用