03 Context

简要介绍

在典型的React应用中,数据是通过props属性自上而下(由父及子)进行传递的
但这种做法对于类似地区偏好,UI主题等全局共享的属性而言是极其繁琐的,这类属性在应用中会被许多组件使用
Context提供了一种在组件之间共享此类值的方式,而无需逐层传递props


何时使用

Context的设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主体或者首选语言

// Class组件使用Context
const ThemeContext = React.createContext('light')

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value='dark'>
        <Toolbar />
      </ThemeContext.Provider>
    )
  }
}
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  )
}
class ThemedButton extends React.Component {
  static contextType = ThemeContext
  render() {
    return <Button theme={this.context}>
  }
}

你也许不需要Context

Context的主要应用场景在于很多不同层级的组件需要访问同样一些数据。
应该谨慎使用,因为使用Context会让降低组件的复用性。

如果只是想避免层层传递一些属性(某个深嵌组件需要使用某个属性),组件组合(Component Compostion)是个比context更好的解决方案。

例如,你有一个Page组件,它层层向下传递user和avatarSize属性,从而深度嵌套的Link和Avatar组件可以使用这些属性

<Page user={user} avatarSize={avatarSize} />
// ...子组件...
<PageLayout user={user} avatarSize={avatarSize} />
// ...子组件...
<NavigationBar user={user} avatarSize={avatarSize} />
// ...子组件...
<NavigationBar user={user} avatarSize={avatarSize} />
// ...渲染出...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize}>
</Link>

像上面这种最终只有Avatar组件真的需要user和avatarSize属性的,那么层层传递这两个props就显得非常冗余,而且一旦Avatar组件需要更多来自顶层组件的props,你还要在中间层一个个加上去,非常麻烦。

一种无需Context的方法就是使用“组件组合”:将Avatar组件自身传递下去,因而中间件无需知道user或者avatarSize等props

function Page(props) {
  const user = props.user
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={avatarSize} />
    </Link>
  )
  return <PageLayout userLink={userLink} />
}
<Page user={user} avatarSize={avatarSize} />
<PageLayout userLink={...}>
<NavigationBar userLink={...}>
{props.userLink}

并且组件不限制接受子组件的个数,你可以像“组件组合”中说的那样为子组件封装多个单独的接口(slots)

function Page(props) {
  const user = props.user
  const content = <Feed user={user} />
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={avatarSize} />
      </Link>
    </NavigationBar>
  )
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  )
}

“组件组合”模式已经可以覆盖大多场景了,但如果子组件在渲染前需要和父组件进行交互,你可以进一步使用render props

优点:

  • 减少了组件中传递的props数量
  • 代码更加简洁干净

缺点:

  • 这种将逻辑提升到组件树更高层会使高层组件复杂

API

React.createContext

const MyContext = React.createContext(defaultValue)

上述语句创建了一个Context对象,当组件A订阅了这个Context时,A会从组件树中距离自身最近的那个匹配的Provider中读取当前context值

只有当组件所处的树中没有匹配到Provider时,defaultValue才会生效。这有助于在不适用Provider包装组件的情况下对组件进行测试。若将undefined传递给Provider的value,consumer组件的defaultValue不会生效。

Context.Provider

每个Context对象都会返回一个Provider组件,它允许Consumer组件订阅Context的变化
Provider接收一个value属性,传递给Consumer组件,一个Provider可以和多个Consumer组件有对应关系。多个Provider也可以嵌套使用,里层的会覆盖外层的数据。

当Provider的value值变化时,其内部所有Consumer组件都会重新渲染。
Provider及其内部Consumer组件不受制于shouldComponentUpdate函数
因此Consumer组件在其祖先节点退出更新时也可以更新

新旧值检测使用了Object.is相同的算法
当value是对象时,可能会倒置一些问题,详见官网

Context.Consumer

<MyContext.Consumer>
  {value => (
    /* something here...*/
  )}
</MyContext.Consumer>

上述是通过function as a child做法让函数式组件订阅Context.
这个函数接收当前的Context值返回一个React元素,value等于Provider的value

Context.displayName

const MyContext = React.createContext(defaultValue)
MyContext.displayName = 'DemoContext'

如上,context对象接受一个String类型、名为displayName的property,用以在React DevTools中显示。

Class.contextType

class Demo extends React.Component {
  componentDidMount() {
    let value = this.context
    /* 组件挂载后 根据MyContext组件值执行一些操作 */
  }
  componentDidUpdate() {
    let value = this.context
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context
    /* ... */
  }
  render() {
    let value = this.context
    /* 基于 MyContext value进行渲染 */
  }
}
Demo.contextType = MyContext

挂载在class上的contextType属性会被重新赋值为context对象
这让你可以使用this.context来读取context的值,你可以在任何声明周期内访问它。

如果你在使用 public class fields 语法,可以使用 static 初始化 contextType

class Demo extends React.Component {
  static contextType = MyContext
  render() {
    let value = this.context
    /* 基于value渲染 */
  }
}

嵌套组件中更新Context

想要在嵌套很深的组件中欧更新context,只需要通过context传递一个函数,深嵌组件即可调用函数更新context

export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {}
})

function ThemeBtn() {
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button onClick={toggleTheme} style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  )
}

示例见myapp>src>components>context


使用多个Context

const ThemeContext = React.createContext('light')
const UserContext = React.createContext({
  name: 'Guest'
})

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props
    // 提供初始 Context 值的 App 组件
    return (
      <ThemeContext.Provider>
        <UserContext.Provider>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    )
  }
}

function Layout() {
  return (
    <div>
      <SideBar />
      <Content />
    </div>
  )
}

function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  )
}

示例见myapp>src>components>context

若两个或者多个context值经常一起使用,就要考虑另外创建一个渲染组件来提供该值。


注意事项

context使用参考标识(reference identity)来决定何时进行渲染,这种做法可能会造成如下问题:
当Provider的父组件重新渲染时,consumers组件会触发意外渲染。如下,value属性总被赋值为新的对象

class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>
        <ToolBar />
      </MyContext.Provider>
    )
  }
}

为了防止这种情况,将value状态提升到父节点的state中去

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      value: {something: 'something'}
    }
  }
  render() {
    <MyContext.Provider value={this.state.value}>
      <ToolBar />
    </MyContext.Provider>
  }
}

示例见myapp>src>components>context

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值