2022年你需要知道的关于React Context的一切,第二部分

321 阅读15分钟

Description: https://images.manning.com/360/480/resize/book/7/a7172e7-f4ca-450d-8460-955f2431f227/Barklund-2ed-MEAP.png

节选自React Quickly, 2版本,作者Morten Barklund

本节选探讨了使用React Context。

如果你是一个React开发者或者想了解更多关于React的信息,请阅读它。


打75折 React Quickly,2ndEditionmanning.com结账时,在折扣代码框中输入fccmardan2,即可享受25%的折扣。


在这里查看第一部分

React上下文的解构

让我们退一步,仔细看看React上下文。如前所述,要使用React context,你需要创建一个提供者和一个消费者。你可以通过两种不同的方式来创建消费者,要么作为一个钩子,要么通过使用 "渲染道具"。但在你做这些事情之前,你需要创建上下文本身。你通过使用存在于React包中的函数createContext

 
 import { createContext } from 'react';
 const MyContext = createContext(defaultValue);
  

这里有两件事要注意。

  • 用大写字母来命名上下文变量是很常见的,因为它有点像React组件的作用(至少它的属性是这样)。
  • createContext 它需要一个参数,也就是默认值。我们将在稍后回到这一点上。

消耗一个上下文

当你有一个上下文变量,例如上面的MyContext,它有两个属性,这就是我们所关心的。MyContext.Provider和MyContext.Consumer。

我们已经解释了如何使用useContext钩子来消费一个上下文。你可以用MyContext.Consumer属性做类似的事情,但这有点棘手。

假设你想在一个名为DisplayName的组件中显示一段由最近的名称上下文提供的名称。我们可以使用useContext钩子来做到这一点,如下图所示。

 
 function DisplayName() {
   const name = useContext(NameContext);
   return <p>{name}</p>
 }
  

这是很简单的。我们调用这个钩子,得到的当前值是一个变量,我们可以直接在组件中使用。

如果我们试图用Consumer 组件做同样的事情,我们必须用一个函数作为第一个也是唯一的一个子函数来调用消费者组件,该函数将用组件的值被调用。

 
 function DisplayName() {
   return (
     <p>
       <NameContext.Consumer>
         {(name) => name}
       </NameContext.Consumer>
     </p>
   );
 }
  

你可能可以看到,这样做的工作量要大得多,如果我们需要对返回的值进行一些计算或逻辑运算,我们就必须对我们的组件进行大量的重组。

在函数式代码库中,使用Consumer 组件是相当罕见的。它可能只在较早的基于类的组件中使用。

上下文的构成

提供者用于创建一个可以被消费的上下文。消费者用于消费最近的提供的上下文。请注意,你可以通过你的应用程序多次提供相同的上下文,你甚至可以提供嵌套的相同上下文。你也可以多次使用相同的上下文,甚至在任何提供者之外。

当你消费一个上下文时,你将得到离你最近的提供者所提供的值,并上升到文件。如果消费者上面没有提供者,你将得到我们创建的上下文时定义的默认值。让我们在图10中说明这一切。


图10 你可以在同一个上下文中拥有许多提供者和消费者。


在图10中,有几件事需要注意。

  • 如果你消费一个上面没有提供者的上下文,如TopComponent ,你将只得到上下文定义中的默认值(本例中为0)。
  • 如果您消费一个上面有多个提供者的上下文,如BottomComponent ,您将从最近的提供者那里获得数值,通过文档树向上寻找(例如,在这种情况下,17而不是2)。

嵌套上下文示例

你可以想象UI变量的嵌套上下文的使用情况。例如,让我们想象一个应用程序,我们在整个应用程序中有不同边框宽度的按钮。

我们的网络应用是一个网店,有不同的购买项目和关于企业的页面。我们在页首和页脚都有一些按钮。我们在页眉和页脚都有打开购物车的按钮。

默认情况下,所有按钮的边界宽度为1像素,但在页脚,所有按钮的边界宽度为2像素。此外,每当我们有一个进入购物车的按钮时,该按钮的边框宽度必须始终为5像素,因为它是一个非常重要的按钮。

让我们先在图11中试着勾勒一下这个系统。


图11 我们的购物网站的组件树。请注意我们如何同时拥有一个默认的上下文值和几个上下文提供者贯穿始终。


现在,每个按钮组件都会在组件树上寻找最近的边框上下文提供者,并使用从那里获取的边框宽度。如果在树上没有找到提供者,按钮将使用原始上下文创建时定义的默认值。

让我们在图12中用所有这些查找最近的提供者的方法对树进行注释。


图12 组件树,每个按钮组件的最近提供者(或根)都有一个较重的箭头,以及该组件的边界宽度。


让我们去实现所有这些,因为我们有所有需要的信息。我们可以在清单5中这样做。

清单5 按上下文划分的边框宽度。

 
 import { useContext, createContext } from 'react';
 const BorderContext = createContext(1);    #A
 function Button({ children }) {
   const borderWidth = useContext(BorderContext);    #B
   const style = {
     border: `${borderWidth}px solid black`,
     background: 'transparent',
   };
   return <button style={style}>{children}</button>
 }
 function CartButton() {
   return (
     <BorderContext.Provider value={5}>    #C
       <Button>Cart</Button>
     </BorderContext.Provider>
   )
 }
 function Header() {
   const style = {
     padding: '5px',
     borderBottom: '1px solid black',
     marginBottom: '10px',
     display: 'flex',
     gap: '5px',
     justifyContent: 'flex-end',
   }
   return (
     <header style={style}>
       <Button>Clothes</Button>
       <Button>Toys</Button>
       <CartButton />
     </header>
   )
 }
 function Footer() {
   const style = {
     padding: '5px',
     borderTop: '1px solid black',
     marginTop: '10px',
     display: 'flex',
     justifyContent: 'space-between',
   }
   return (
     <footer style={style}>
       <Button>About</Button>
       <Button>Jobs</Button>
       <CartButton />
     </footer>
   )
 }
 function App() {
   return (
     <main>
       <Header />
       <h1>Welcome to the shop!</h1>
       <BorderContext.Provider value={2}>    #D
         <Footer />
       </BorderContext.Provider>
     </main>
   );
 }
  

#A 我们创建初始上下文,默认值为1

#B 在按钮组件中,我们使用最近的提供者提供的任何数值,并将其作为CSS中的边框宽度属性。

#C 我们在购物车按钮的周围添加一个边框宽度提供者,为这个按钮提供精确的5px

#D 我们在页脚周围添加一个提供者,确保里面的所有按钮默认都有2px的边框,除非有其他更具体的提供者告诉他们。

源代码

你可以从github.com/rq2e/rq2e/t…上下载上述例子的源代码,或者你可以通过运行下面的代码来初始化一个新的React项目,并预先填入这个例子。

 
 npx create-react-app rq10-border-context --template rq10-border-context
  

在浏览器中打开后,我们会看到图13。


图13 我们的商店网站显示了所有的按钮,其宽度与设计的完全一致。这看起来并不好,但出于某种原因,这正是客户所希望的


把上下文提高到新的水平

到目前为止,我们已经把数字和字符串放到了上下文中,但没有什么能阻止我们把对象或函数放到其中。事实上,你甚至可以把一个由许多其他值组成的复杂对象放在里面,并混合各种类型。

一个常见的方法是使用一个上下文作为有状态值和设置器的传递机制。

例如,让我们想象一下,我们有一个具有黑暗模式和光明模式的网站。我们在页首有一个按钮,可以在两者之间切换。所有相关的组件将查看当前的状态,并根据这个状态值来改变它们的设计。

我们想把两样东西放到状态中。一个告诉我们当前是否处于黑暗模式的值(isDarkMode),和一个允许按钮在两种模式之间切换的函数(toggleDarkMode)。我们可以把这两个值放在一个单一的对象中,并把它作为值塞进上下文。

我们将在图14中勾勒这个系统。


图14 我们网站的文档树草图,有一个黑暗模式/光明模式的切换。请注意我们是如何将一个对象传递给上下文提供者的,然后我们可以对其进行解构,并在我们需要这两个值的地方使用提供者下面整个文档树中的任何一个组件。


让我们继续在清单6中实现这一点。

清单6 带有上下文的黑暗模式。

 
 import { useContext, useState, createContext, memo } from 'react';
 const DarkModeContext = createContext({});    #A
 function Button({ children, ...rest }) {
   const { isDarkMode } = useContext(DarkModeContext);    #B
   const style = {
     backgroundColor: isDarkMode ? '#333' : '#CCC',
     border: '1px solid',
     color: 'inherit',
   };
   return <button style={style} {...rest}>{children}</button>
 }
 function ToggleButton() {
   const { toggleDarkMode } = useContext(DarkModeContext);    #C
   return <Button onClick={toggleDarkMode}>Toggle mode</Button>
 }
 const Header = memo(function Header() {
   const style = {
     padding: '10px 5px',
     borderBottom: '1px solid',
     marginBottom: '10px',
     display: 'flex',
     gap: '5px',
     justifyContent: 'flex-end',
   }
   return (
     <header style={style}>
       <Button>Products</Button>
       <Button>Services</Button>
       <Button>Pricing</Button>
       <ToggleButton />
     </header>
   )
 });
 const Main = memo(function Main() {    #D
   const { isDarkMode } = useContext(DarkModeContext);    #B
   const style = {
     color: isDarkMode ? 'white' : 'black',
     backgroundColor: isDarkMode ? 'black' : 'white',
     margin: '-8px',
     minHeight: '100vh',
     boxSizing: 'border-box',
  
   }
   return <main style={style}>
     <Header />
     <h1>Welcome to our business site!</h1>
   </main>
 });
 function App() {
   const [isDarkMode, setDarkMode] = useState(false);    #E
   const toggleDarkMode = () => setDarkMode(v => !v);    #E
   const contextValue = { isDarkMode, toggleDarkMode };    #F
   return (
     <DarkModeContext.Provider value={contextValue}>    #G
       <Main />
     </DarkModeContext.Provider>
   );
 }
  

#A 这一次我们用一个空对象来初始化我们的上下文。这是因为我们知道我们将永远在应用程序的根部有一个上下文,所以默认值将永远不会被使用。

#B 在这两个位置我们只使用上下文中的isDarkMode标志

#C 在切换按钮中,我们只使用上下文中的toggleDarkMode函数

#D 我们对主组件进行备忘

#E 在主应用组件中,我们定义了两个进入上下文的值

#F 我们把这两个值放在一个单一的对象中

#G 我们使用这个单一对象作为我们的上下文提供者的值

源代码

你可以从github.com/rq2e/rq2e/t…上下载上述例子的源代码,或者你可以通过运行下面的代码来初始化一个新的React项目,并预先填入这个例子。

 
 npx create-react-app rq10-dark-mode --template rq10-dark-mode
  

我们可以在图15中观察这个网站的运行情况。


图15 我们的网站分别在浅色模式和深色模式下。


这里需要注意的是我们如何在例子中的#E和#F行为这个上下文提供两个不同的属性,同时也要注意我们如何在#D行将上下文提供者中的一些组件备忘化。这一点非常重要,因为我们的主App组件会在每次上下文发生变化时重新渲染,也就是在每次切换黑暗模式标志时(因为状态更新)。然而,我们并不希望所有其他的组件因为上下文的变化而重新渲染。在这个例子中,Main组件消耗了上下文,所以它将在每次上下文更新时重新渲染,但Header不消耗上下文,所以它不应该重新渲染。在我们使用记忆化的情况下,它不会,所以这基本上是完美的。

而且,我们不必止步于此。我们可以把一大堆的属性和函数放到上下文值中。事实上,围绕着使用上下文的功能提供者有一个完整的概念,叫做提供者模式。在barklund.dev/provider上有一篇关于这种模式的长篇文章,有很多细节。

上下文选择器

在前面的例子中,我们的上下文提供者有一个小的次优问题。这个问题是,当上下文中的任何值发生变化时,所有消耗特定上下文的组件都会重新渲染。

这是因为我们的上下文现在是一个具有多个属性的复杂对象,但React并不真正关心这个。React只是看到上下文的值发生了变化,所以使用该上下文的每个组件都会被重新渲染。

然而,我们的toggle组件永远不需要重新渲染。切换组件使用了一个可以被备忘录化的函数,以达到完全稳定。toggleDarkMode 函数不依赖于上下文的当前值,所以它可以被备忘录化以保持稳定。

不幸的是,我们不能告诉React在上下文的特定属性更新时只重新渲染一个特定的组件。至少,我们还不能这样做。这一点肯定会在React的未来更新中出现,但现在还没有。它预计会在React 18中出现,但没有实现。

因此,如果我们想做到这一点,我们需要使用一个外部库。这个库叫做use-context-selector ,它允许我们不只是每次都使用整个上下文,而是指定我们感兴趣的上下文中的特定属性(我们 "选择 "相关的属性,所以叫选择器),现在React只会在该特定属性变化时重新渲染我们的组件。

为了正确使用use-context-selector 包,我们还需要使用这个包来创建我们的上下文。我们不能使用React包中由createContext 创建的常规上下文,而是要使用use-context-selector 包提供的createContext 函数。

让我们继续,在清单7中实现我们的黑暗模式切换网站的这个更新和更优化的版本。

清单7 带有上下文选择器的黑暗模式。

 
 import { useState, useCallback, memo } from 'react';
 import { createContext, useContextSelector } from 'use-context-selector';    #A
 const DarkModeContext = createContext({});
 function Button({ children, ...rest }) {
   const isDarkMode = useContextSelector(    #B
     DarkModeContext,
     (contextValue) => contextValue.isDarkMode,    #C
   );
   const style = {
     backgroundColor: isDarkMode ? '#333' : '#CCC',
     border: '1px solid',
     color: 'inherit',
   };
   return <button style={style} {...rest}>{children}</button>
 }
  
 function ToggleButton() {
   const toggleDarkMode = useContextSelector(    #B
     DarkModeContext,
     (contextValue) => contextValue.toggleDarkMode,    #C
   );
  
   return <Button onClick={toggleDarkMode}>Toggle mode</Button>
 }
  
 const Header = memo(function Header() {
   const style = {
     padding: '10px 5px',
     borderBottom: '1px solid',
     marginBottom: '10px',
     display: 'flex',
     gap: '5px',
     justifyContent: 'flex-end',
   }
   return (
     <header style={style}>
       <Button>Products</Button>
       <Button>Services</Button>
       <Button>Pricing</Button>
       <ToggleButton />
     </header>
   )
 });
  
 const Main = memo(function Main() {
   const isDarkMode = useContextSelector(    #B
     DarkModeContext,
     (contextValue) => contextValue.isDarkMode,    #C
   );
   const style = {
     color: isDarkMode ? 'white' : 'black',
     backgroundColor: isDarkMode ? 'black' : 'white',
     margin: '-8px',
     minHeight: '100vh',
     boxSizing: 'border-box',
  
   }
   return <main style={style}>
     <Header />
     <h1>Welcome to our business site!</h1>
   </main>
 });
  
 function App() {
   const [isDarkMode, setDarkMode] = useState(false);
   const toggleDarkMode = useCallback(() => setDarkMode(v => !v), []);    #D
   const contextValue = { isDarkMode, toggleDarkMode };
   return (
     <DarkModeContext.Provider value={contextValue}>
       <Main />
     </DarkModeContext.Provider>
   );
 }
  

#A 这次我们只改变了一些东西。我们从use-context-selector包中导入了两个函数

#B 每当我们需要一个来自上下文的值时,我们就使用新的useContextSelector钩子,而不是普通的useContext钩子。

#C 除了要选择的上下文之外,我们还提供了一个函数来指定我们对上下文的哪一部分感兴趣。现在React将确保只在上下文的这个特定部分更新时重新渲染这个组件。

#D 最后,我们需要使用useCallback来记忆我们的切换函数。

源代码

你可以从github.com/rq2e/rq2e/t…上下载上述例子的源代码,或者你可以通过运行下面的代码来初始化一个新的React项目,并预先填入这个例子。

 
 npx create-react-app rq10-dark-mode-selector --template rq10-dark-mode-selector
  

这样做的结果和我们之前的网站完全一样,功能也完全一样--只是现在ToggleButton ,永远不会重新渲染,因为它只使用一个来自上下文的稳定值,这个值永远不会更新,所以不需要重新渲染这个组件。在上下文中监听isDarkMode 标志的两个组件仍然会在每次标志更新时重新渲染,因为我们在这两个组件的useContextSelector 钩子中选择了那个确切的属性。

在这一点上,这可能看起来是一种过度优化,因为我们讨论的是单个组件是否会额外更新几次。然而,在一个有许多上下文的大型应用中,这就增加了。而且很快就会增加!因此,如果你在整个应用程序中使用上下文来共享共同的功能,你应该使用useContextSelector ,而不是正常的useContext 钩子。除非在你读到这篇文章的时候,React已经将选择逻辑作为普通的useContext 钩子的一部分来实现了。

这有什么用呢?

这似乎是一个次要的模式,对于一些功能来说可能是很聪明的,但它的作用远不止于此。这种模式实际上可以作为一种单一的方式,在你的整个应用程序中分配和组织数据和功能。

你的应用程序可以在许多不同的层上有几十个不同的上下文,它们相互作用,为你的部分或全部应用程序提供全局和局部功能。例如,你可以把你的用户授权与当前用户信息以及登录和注销的方法放在一个上下文中,把你的应用程序数据放在第二个上下文中,把控制用户界面的数据放在第三个上下文中。

如果使用得当,这是你的React工具箱中最强大的模式,你可以将其应用于几乎任何应用程序。它非常通用,用途广泛,可以应用于任何情况;而且它的可定制性足以对许多不同的结构有用。

有些人可能会认为,与其使用React Context与useContextSelector 来管理整个应用程序的复杂状态,不如使用一个成熟的工具,如redux-toolkit 来提供这个功能。但实话实说:redux-toolkit 在引擎盖下使用这个确切的功能来提供它的魔力,所以你使用这两种方法都会得到完全相同的性能。

这篇文章就写到这里。谢谢你的阅读。

The postEverything You Need to Know about React Context in 2022, Part 2appeared first onManning.