【React系列】你真的会用useContext吗?

4,090 阅读3分钟

此文章不适合没hooks经验的同学,新手可以先看这个

一、useContext

1.1 React.createContext

createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息(也就是value对象)。

1.2 useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

🍊🍐一:

import React,{useState,createContext,useContext, memo, useMemo} from 'react';


const ParentContext = createContext();

export default function Parent(){
  const [show, setShow] = useState(false)
  return (
    <React.Fragment>
      <ParentContext.Provider value={setShow}>
        <Child></Child>
      </ParentContext.Provider>
      <Other show={show} setShow={setShow}></Other>
    </React.Fragment>
  )
}

function Child(props){
  // O === setShow 方法
  const O = useContext(ParentContext)
  console.log('Child 组件执行了');
  return (
    <button onClick={()=>{
      O(true)
    }}>Child</button>
  )
}

function Other(props){
    const { show, setShow} = props
    return (
       show ? <button onClick={()=>{setShow(false)}}>Other</button> : ''
    )
}

这样只是简单的把功能实现,注意这里会有一个问题,Parent每次组件的执行都会引发子组件的渲染。这是因为父组件更新子组件也会执行。

二、useContext性能优化

2.1 为了解决以上组件的开销较大问题,可以使用memoization 来优化。

*memoization定义:如果函数组件在给定相同 props 的情况下,那么可以通过 React.memo将其包装,以此记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

🍊🍐:

import React,{useState,createContext,useContext, memo, useMemo} from 'react';

const ParentContext = createContext();

export default function Parent(){
  const [show, setShow] = useState(false)
  return (
    <React.Fragment>
      <ParentContext.Provider value={setShow}>
        <Child></Child>
      </ParentContext.Provider>
      <Other show={show} setShow={setShow}></Other>
    </React.Fragment>
  )
}

const Child = memo(function Child(props){
  const O = useContext(ParentContext)
  console.log('Child 组件执行了');
  last = O;
  return (
    <button onClick={()=>{
      O(true)
    }}>Child</button>
  )
})

const Other = memo(
  function Other(props){
    const { show, setShow} = props
    return (
       show ? <button onClick={()=>{setShow(false)}}>Other</button> : ''
    )
  }
)

真的解决了吗?继续看👀

接着修改如下,这时候再去看控制台,是不是心里mmp了,Child又莫名重新渲染了,是为什么呢?

import React,{useState,createContext,useContext, memo, useMemo} from 'react';

const ParentContext = createContext();

export default function Parent(){
  const [show, setShow] = useState(false)

  return (
    <React.Fragment>
      <ParentContext.Provider value={{setShow}}>
        <Child></Child>
      </ParentContext.Provider>
      <Other show={show} setShow={setShow}></Other>
    </React.Fragment>
  )
}

let last;
const Child = memo(function Child(props){
  const {setShow} = useContext(ParentContext)
  console.log('Child 组件执行了');
  console.log('last === setShow', last === setShow) //
  last = setShow;
  return (
    <button onClick={()=>{
      setShow(true)
    }}>Child</button>
  )
})

const Other = memo(
  function Other(props){
    const { show, setShow} = props
    return (
       show ? <button onClick={()=>{setShow(false)}}>Other</button> : ''
    )
  }
)

官方说明

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。可以理解为useContext可以击穿一切即使是memo。

大概原理:ParentContext.Provider 属性value我改变成了一个对象,当每次执行setShow方法,Parent都会重新渲染,value这个对象都会重新生成,由于ReactObject.is()浅比较,即useContext(ParentContext)返回的对象每次都是不等的(可以在Child组件打印看看),所以又重新渲染Child,造成不必要的开销。

既然有问题,那怎么解决呢?接下来大招来了。🤭🤭🤭

useMemo上场:

export default function Parent(){
  const [show, setShow] = useState(false)
  // 只需要修改这里通过useMemo包装,保证每次返回的对象不变,deeps为[],只在首次挂载执行,所以就保证了每次value都是同一个。
  const ctxValue = useMemo(() => ( {setShow }), [])

  return (
    <React.Fragment>
      <ParentContext.Provider value={ctxValue}>
        <Child></Child>
      </ParentContext.Provider>
      <Other show={show} setShow={setShow}></Other>
    </React.Fragment>
  )
}

三、 总结

  • useContext使用很简单,但是想用好还是有很多麻烦的,除非不关心性能。
  • 子组件使用memo做记忆缓存。
  • 使用useMemo包装,控制返回contextValue对象根据依赖deep改变。

如果有错误,欢迎指正,多多讨论。

THK!!!