六、useContext深入学习

一、介绍

useContext是React hooks三个最基础的hook函数之一。

const value = useContext(MyContext);

useContext接收一个 context 对象React.createContext 的返回值)并返回该 context 的当前值

当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化

注意:useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

二、Context的使用

首先看看context的这几个api。

1、React.createContext
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

2、React.Provider
<MyContext.Provider value={/* 某个值 */}>

​ 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化

​ Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

​ 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

通过新旧值检测来确定变化,使用了与 Object.is 相同的算法

当传递对象给 value 时,检测变化的方式会导致一些问题:因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

<MyContext.Provider value={{something: 'something'}}>       
  <Toolbar />
 </MyContext.Provider>

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

3、React.Consumer
<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

​ 这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context

​ 这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

补充:

如果只有一个consumer,那么就可以使用contextType来代替consumer

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。   
    // 无论多深,任何组件都能读取这个值。    
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。    
    return (
          <ThemeContext.Provider value="dark">        
                <Toolbar />
          </ThemeContext.Provider>
        );
      }
}
 
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {  return (
    <div>
      <ThemedButton />
    </div>
  );
}
 
class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。  
  // React 会往上找到最近的 theme Provider,然后使用它的值。  
  // 在这个例子中,当前的 theme 值为 “dark”。  
    static contextType = ThemeContext;
      render() {
        return <Button theme={this.context} />;  
    }
}
4、动态Context

​ 可以在React.createContext();绑定动态值。

​ 在嵌套组件中更新 Context:从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context。

// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const ThemeContext = React.createContext({
theme: themes.dark,  toggleTheme: () => {},});
//创建Context组件
const ThemeContext = React.createContext({
  theme: 'dark',
  toggle: () => {}, //向上下文设定一个回调方法
});

//运行APP
class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggle = () => { //设定toggle方法,会作为context参数传递
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    this.state = {
      theme: themes.light,
      toggle: this.toggle,
    };
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state}> //state包含了toggle方法
        <Content />
      </ThemeContext.Provider>
    );
  }
}

//中间组件
function Content() {
  return (
    <div>
      <Button />
    </div>
  );
}

//接收组件
function Button() {
  return (
    <ThemeContext.Consumer>
      {({theme, toggle}) => (
        <button
          onClick={toggle} //调用回调
          style={{backgroundColor: theme}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

context在如下的生命周期钩子中可以使用

  • constructor(props, context)
  • componentWillReceiveProps(nextProps, nextContext)
  • shouldComponentUpdate(nextProps, nextState, nextContext)
  • componentWillUpdate(nextProps, nextState, nextContext)
  • componentDidUpdate(prevProps, prevState, prevContext)

React context的局限性

  1. 在组件树中,如果中间某一个组件 ShouldComponentUpdate 返回了 false ,会阻碍 context 的正常传值,导致子组件无法获取更新。
  2. 组件本身 extends React.PureComponent 也会阻碍 context 的更新。

三、useContext的出现是为了解决什么问题

​ Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。如果我们在一个项目中只会出现一个context,那么consumer订阅provider的方式完全可以使用contextType来代替。但事实上,在一个项目势必会涉及到很多context,如果当前用户,国际化,主题颜色等。这么多的context,如果在某一个组件中同时使用了这三者,那么会造成很深的嵌套,不仅后期维护困难,而且开发体验也不会太好。

​ 所以为了解决这个问题,就有useContext的出现。useContext与useState、useEffect是react hooks的三大基本hooks。但是useContext与其他两者不一样,他并没有完全摒弃之前使Context的方式方法,而是在之前使用context的基础上做了一个优化。

const ThemeContext = React.createContext('light');
const localContext = React.createContext('zh');
const App=()=>{
  return (
          <ThemeContext.Provider value="dark">        
                <Toolbar />
          </ThemeContext.Provider>
        );
	}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {  return (
    <div>
      <ThemedButton />
    </div>
  );
}
 
const ThemedButton()=>{
  const theme=useContext(ThemeContext);
  const local=useCOntext(localContext);
   return(
    	<Button theme={this.context}/>
		)
}

四、Context的优缺点

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

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

我们考虑这样的场景:

​ 我们有一个Page组件,在Page组件中有一个Link组件,然后Link组件中有一个User组件,那么数据需要从Page组件传递到User组件,而中间的Link组件,不对数据做任何的处理。只是透传到User组件。但是当需要有需求变动的时候,还需要对Link组件作出更新,那么可以使用React.Children的方式,也就是slot的方式将组件组合在一起:

const Page = () => {
    return (
        <div></div>
    )
}
const Link = () => {
    return (
        <div></div>
    )
}
const User = () => {
    return (
        <div></div>
    )
}
const data = {name: 'lee'}
const App = () => {
    return (
        <Page>
            <Link user={data}>
                <User user={data}></User>
            </Link>
        </Page>
    )
}

​ 这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)”.

​ 这种模式足够覆盖很多场景了,在这些场景下你需要将子组件和直接关联的父组件解耦。如果子组件需要在渲染前和父组件进行一些交流,你可以进一步使用 render props

​ 但是,有的时候在组件树中很多不同层级的组件需要访问同样的一批数据。Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据,这比替代方案要简单的多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码搬运工_田先森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值