通过示例解释 React 中的 Scoped Context

**React Context 是一个全局变量**

在 Javascript 中,变量的作用域在函数定义内。

通过示例解释 React 中的作用域上下文

React Context 通常被描述为一种管理全局状态的机制,充当可跨 React 组件树访问的共享变量。虽然这种描述是准确的,但它过于简化了 Context 的功能。在本文中,我们将深入探讨如何有效地限定 Context 的范围,确保它只在需要的地方使用,并避免不必要的重新渲染。

什么是 React Context?

React Context 提供了一种通过组件树传递数据的方法,而无需在每个级别手动传递 props。它是使用 `React.createContext` 创建的,由 `Provider` 和 `Consumer` 对组成。`Provider` 组件提供值,任何用 `Consumer` 或 `useContext` 钩子包装的组件都可以访问它。

这是一个基本的例子:

import React, { createContext, useContext } from "react";

const ThemeContext = createContext("light");

function App() {
  return (
    
      
    
  );
}

function Toolbar() {
  return ;
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return ;
}

export default App;

在这个例子中,`ThemedButton` 可以访问 `ThemeContext.Provider` 提供的 `theme` 值,而无需通过 `Toolbar` 明确传递 props。

为什么要使用作用域上下文?

虽然 Context 功能强大,但滥用它可能会导致性能问题。当 `Context.Provider` 提供的值发生变化时,所有使用该上下文的组件都将重新渲染。在复杂的应用程序中,这可能会导致不相关的组件不必要的重新渲染。

Scoped Context 是指将 Context 的使用限制在组件树中真正需要的部分的做法。这种方法有助于保持性能,并使组件结构保持干净和易懂。

复合部件面临的挑战

考虑涉及复合组件的场景,例如 Radix Primitives 等库提供的组件。这些组件通常在内部使用 Context 来管理状态和交互。但是,当类似的组件组合在一起时可能会出现问题,导致上下文冲突。

基数原语示例

Radix Primitives 提供了高度可组合的 API,用于构建可访问的组件。以下是示例:


  
    
    
       {/* note the alert trigger in dialog content */}
    
  

  

这里出现了一个问题,因为 `AlertDialog` 是 `Dialog` 的组合,并附加了满足 `AlertDialog` 要求的功能。这意味着 `AlertDialog.Root` 也是一个 `Dialog.Root`,因此它同时提供了 `DialogContext` 和 `AlertDialogContext`。

在此设置中,`AlertDialog.Trigger`(也是 `Dialog.Trigger`)可能会通过 `useContext(DialogContext)` 检索错误的上下文,最终会从 `Dialog.Root` 而不是 `AlertDialog.Root` 获取上下文。因此,单击 `AlertDialog.Trigger` 可能会切换 `Dialog.Content`,而不是按预期运行。

Scoped Context 解决方案

为了防止此类问题,Radix Primitives 使用了作用域上下文。作用域上下文确保 `AlertDialog.Trigger` 仅与 `AlertDialog` 部分交互,并且不会意外地从类似组合的组件中检索上下文。这是通过在内部创建新上下文并通过自定义 prop(例如 `__scopeDialog`)将其传递给 `Dialog` 组件来实现的。然后,`Dialog` 组件在其 `useContext` 调用中使用此作用域上下文,从而确保隔离。

来自 radix ui github repo 的源代码:

https://github.com/radix-ui/primitives/blob/dae8ef4920b45f736e2574abf23676efab103645/packages/react/dialog/src/Dialog.tsx#L69

以下是 Radix UI 中作用域上下文的工作方式的抽象:

  • 范围创建:createScope 实用程序为每个组件或复合组件生成一个唯一的命名空间。这可确保每组上下文都是独立的,不会与其他上下文冲突。从 '@radix-ui/react-context' 导入 { createScope };const [createDialogContext, useDialogScope] = createScope('Dialog');
  • 作用域提供程序:创建上下文时,它们与作用域绑定。这会将提供程序和消费者绑定到同一个命名空间。const DialogContext = createDialogContext('DialogContext');
  • 消费者隔离:范围钩子,比如 useDialogScope,确保消费者只访问来自其预期范围的上下文。const useDialogContext = (scope) => useContext(DialogContext, useDialogScope(scope));
  • 作用域上下文的好处

  • 上下文冲突预防:通过限定上下文范围,像 AlertDialog.Trigger 这样的组件总是能够找到它们关联的上下文(AlertDialogContext),即使嵌套在其他上下文中。
  • 灵活的组合:范围上下文支持灵活、安全的组件组合,确保交互保持可预测。
  • 可重用性:开发人员可以在不同范围内重用通用组件(例如,Dialog.Trigger),无需修改。
  • 如何应用于示例

    在您的示例中:

  • AlertDialog.Root 创建一个范围的 AlertDialogContext 来封装其状态和交互。
  • 嵌套的 Dialog.Root 和 AlertDialog.Trigger 可以无冲突地共存,因为它们各自引用其各自的范围上下文。
  • 这种设计模式是 Radix UI 的一个关键特性,可确保复杂的组件层次结构无缝运行,不会出现意外行为。
  • 参考:

  • https://dev.to/romaintrotard/use-context-selector-demystified-4f8e
  • https://github.com/radix-ui/primitives
  • https://react.dev/reference/react/createContext