[特殊字符] 一行代码,优雅的终结 React Context 嵌套地狱!

什么是 Context 嵌套地狱?

React Context 嵌套地狱是指 React Context Provider 的多层嵌套,如下图所示,类似于前端问题中的 回调地狱 一样,越来越多的 Context 会导致嵌套层数越来越大,导致代码阅读性极差。

为什么会写出 Context 嵌套地狱这种代码?

Context 是 React 的上下文状态管理 API,允许我们跨组件实现状态透传,从而达到状态共享的目的。但是 Context 存在性能问题,就是 当 Context 包含多个状态属性时,当修改了其中的状态,由于 React 的 re-render 特性, 所有依赖该 Context 的组件都会重新渲染,即使某些组件所依赖该 Context 的状态值并没有变。代码示例如下所示:

定义 Context(context.ts):

 
import { createContext } from 'react'

export const AppContext = createContext<{
  theme: 'dark' | 'light',
  count: number,
  increase: () => void
}>({
  theme: 'dark',
  count: 0,
  increase() {}
})

提供和消费 Context(page.tsx):

 
import { use, useState } from "react"
import { AppContext } from "./context"

export default function App() {
  const [count, setCount] = useState(0)
  const [theme, setTheme] = useState<'dark' | 'light'>('dark')
  return (
    <AppContext.Provider value={{ count, theme, increase: () => setCount(count + 1) }}>
      <Header />
      <Button />
    </AppContext.Provider>
  )
}

function Header() {
  const { theme } = use(AppContext)
  console.log('Header rendered')
  return (
    <header className={theme}>Header rendered, cur theme: {theme}</header>
  )
}

function Button() {
  const { count, increase } = use(AppContext)
  console.log('Button rendered')
  return (
    <button onClick={increase}>you click me {count} times.</button>
  )
}

上面代码中,每次点击 Button 修改 AppContext 中的 { count: 0} 状态,即使 Header 并没有消费 count,但也都会重新渲染一遍,这是因为 Header 组件消费(订阅)了 AppContext,React 会更新订阅了该 context 的组件。当状态 count 改变时,触发 re-render,传入子组件中的 “context” 状态也都是新的对象,触发 context 的订阅更新。

那么如何解决呢?关键在于状态是否改变,我们要让不需要更新的组件的状态保持不变。为此我们 需要将 context 拆分到各自独立的 Provider 组件中,当状态在各自的 Provider 中发生改变时,React 会自上而下进行更新,并且只会将消费了这个 context 的组件进行重新渲染,相当于做到了状态隔离。

示例代码如下:

(1)拆分 context(context.ts):

 
import { createContext } from 'react'

// 定义 CounterContext
export const CounterContext = createContext<{
  count: number,
  increase: () => void
}>({
  count: 0,
  increase() {}
})

// 定义 ThemeContext
export type Theme = 'dark' | 'light'
export const ThemeContext = createContext<{
  theme: Theme,
  toggle: () => void
}>({ theme: 'dark', toggle() {} })

(2)拆分 Provider,状态隔离:

 
import { use, useState } from "react"
import { CounterContext, Theme, ThemeContext } from "./context"

// 提供 ThemeContext
function ThemeContextProvider({ children } : { children ?: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('dark')
  const toggle = () => setTheme(theme === 'dark' ? 'light' : 'dark')
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  )
}

// 提供 CounterContext
function CounterContextProvider({ children } : { children ?: React.ReactNode }) {
  const [count, setCount] = useState(0)
  const increase = () => setCount(count + 1)
  return (
    <CounterContext.Provider value={{ count, increase }}>
      {children}
    </CounterContext.Provider>
  )
}

export default function App() {
  return (
    <ThemeContextProvider>
      <CounterContextProvider>
        <Header />
        <Button />
      </CounterContextProvider>
    </ThemeContextProvider>
  )
}

// 消费 ThemeContext
function Header() {
  const { theme, toggle } = use(ThemeContext)
  console.log('Header rendered')
  return (
    <header className={theme} onClick={toggle}>Header rendered, cur theme: {theme}</header>
  )
}

// 消费 CounterContext
function Button() {
  const { count, increase } = use(CounterContext)
  console.log('Button rendered')
  return (
    <button onClick={increase}>you click me {count} times.</button>
  )
}

然后随着 context 越来越多,嵌套层数也会越来越大,这就形成了 Context 嵌套地狱问题。

 
export default function Page() {
  return (
    <ThemeContextProvider>
      <CounterContextProvider>
        <OtherContextProvider>
          // ... 更多嵌套 context
          <App />
        </OtherContextProvider>
      </CounterContextProvider>
    </ThemeContextProvider>
  )
}

备注:这里解决 context 的 re-render 问题的方案还有 memouseMemo 等,就不列举了,一方面是本文重点在于介绍嵌套地狱,另一方面在于众所周知 React 哲学之一就是能不上优化相关的 hooks 就不上,提高代码可维护性和阅读性。

一行代码优雅的解决 Context 嵌套地狱

我们 观察上面多层嵌套的组件形式,可以看到其实就是从最上层组件一直往里塞子组件,子组件里再塞子组件,就是不断创建子组件套娃的过程。 有什么可以手动往组件中嵌入子组件的方法?答案是 React.cloneElement,它用于克隆一个已有的 React 元素(ReactElement),并可以为其添加新的 props 或修改现有的 props,并可以添加子组件。

下面是 React.cloneElement 方法定义:

 
React.cloneElement(element, [props], [...children])

  • element: 被克隆的 React 元素
  • props: 可选参数,一个对象,包含要添加到克隆元素的新属性或要覆盖的现有属性
  • children: 可选参数,新的子元素(组件),会替换原先的子元素

那么原来的多层嵌套代码就可以被打平了:

 
export default function Page() {
  const comp1 = React.cloneElement(<OtherContextProvider />, {}, <><Header /><Button /></>)
  const comp2 = React.cloneElement(<CounterContextProvider />, {}, comp1)
  const comp3 = React.cloneElement(<ThemeContextProvider />, {}, comp2)
  return comp3
}

可以看出,上面的代码可以用 reduceRight 方法优化:

 
export default function Page() {
  return [
    <ThemeContextProvider />,
    <CounterContextProvider />,
    <OtherContextProvider />,
    <><Header /><Button /></>
  ].reduceRight((pre, cur) => React.cloneElement(pre, {}, cur))
}

我们继续封装得到 MultiProviders 函数,终于点题了!这个方法核心只有简单一行代码,这就是解决方案 🎉🎉🎉!

 
/**
 * 封装多层 Provider
 *
 * @param providers
 * @returns 组件树
 */
type MultiProvidersPropsType = { providers: React.ReactElement[], children: React.ReactNode}
function MultiProviders({ providers, children } : MultiProvidersPropsType) {
  return (<>
    { providers.reduceRight((pre, cur) => React.cloneElement(cur, {}, pre), children) }
  </>)
}

export default function Page() {
  return (
    <MultiProviders providers={[
      <ThemeContextProvider />,
      <CounterContextProvider />,
      <OtherContextProvider />
    ]}>
      <App />
    </MultiProviders>
  )
}

function App() {
  return (<>
    <Header />
    <Button />
  </>)
}

它的特性:

  1. 代码结构清晰,无嵌套结构
  2. 拆分 context,状态隔离,避免非必要渲染

总结

  • 总体思路:多个状态合并到一个 Context 管理会导致非必要更新问题 => 拆分 Context、Provider 避免非必要更新 => 存在嵌套地狱问题 => 组件的嵌套实际上是子孙组件的不断创建 => React.cloneElement + reduceRight 循环创建子孙组件

  • 当然了,如果是较为复杂的状态管理,最直接的解决方式是不使用 Context,根据具体情况可使用 React.useReducer 和 Redux/Mobx/Valtio/Jotai/Zustand 等状态管理库。

原文:https://juejin.cn/post/7483406014390763574

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值