解决React应用中道具钻研的更好方法

155 阅读15分钟

在React应用程序中处理状态管理是一件很棘手的事情,特别是当数据需要从一个根组件向下传递到深度嵌套的组件时。作为React的开发者,我们经常倾向于过度设计我们的应用程序,在实际不需要的情况下过分依赖Context API和Redux。我们过快地使用这些工具--即使是在只需要向深度嵌套的组件传递状态/数据的基本情况下--都是为了克服道具的钻研。

这在某些情况下是完全没问题的,但在其他情况下,它给我们的应用增加了冗余。每一个消耗或使用这些提供者的组件,只要有一个状态变化,就会被重新渲染。

很少有开发者停下来看看React库本身的一些问题的解决方案--甚至考虑是否有更好的替代方案来顺着组件树传递数据--结果,我们没有看透React的表面定义,即它是一个构建用户界面的JavaScript库。

但React本身也是一个状态管理库,它为状态管理提供了自己方便的解决方案,特别是对于将数据向下传递到深度嵌套的组件这样的事情。这篇文章旨在为你提供一个清晰的指南,告诉你如何去做这件事--并展示了更有选择地依赖Context API或Redux的好处。

什么是道具钻取,为什么是个问题?

如果不先看一下问题本身,我们就不能看问题的解决方案。那么,到底什么是道具钻取,为什么会有问题?

支撑钻取是一个非正式的术语,指的是通过几个嵌套的子组件传递数据,以便将这些数据传递给一个深度嵌套的组件。这种方法的问题在于,大多数通过这些数据的组件对这些数据没有实际的需求。它们只是被用作媒介,将这些数据传送到目标组件。

这就是 "钻井 "一词的由来,因为这些组件被迫接受不相关的数据并将其传递给下一个组件,而下一个组件又将其传递,如此反复,直到到达目的地。这可能会对组件的可重用性和应用程序的性能造成重大问题,我们将在后面解释。

现在,让我们来看看可能导致道具钻取的一组示例情况。

构建一个深度嵌套的应用程序来进行道具钻取

想象一下,我们正在构建一个应用程序,当用户登录时,他们会根据名字来欢迎他们。下面是我们将看到的演示应用程序的视觉表现。

A visual diagram of our app's structure

我们将不涉及造型,以保持我们的代码最小化;这只是为了提供一个关于我们的应用程序看起来像什么的坚实概念。

现在,让我们看一下组件的层次结构,以了解组件之间的关系。

Diagram of our component hierarchy

正如你现在可能看到的,我们遇到的问题是,保存用户姓名的user 对象只在根组件层**(App**)可用,而呈现欢迎信息的组件则嵌套在我们的应用程序**(Message**)深处。这意味着我们必须以某种方式将这个user 对象向下传递给渲染欢迎信息的组件。

蓝色箭头代表实际的user 对象,它从根App组件向下钻,通过几个嵌套组件,到需要它的实际Message组件。然后,它最终渲染了带有登录用户姓名的欢迎信息。

这是一个典型的道具钻取的案例。在这种情况下,开发者经常求助于Context API作为绕过这个所谓问题的手段,而没有考虑到其中产生的潜在问题。

现在我们有了一个项目的视觉地图,让我们用实际的代码来弄脏我们的手。

import { useState } from "react";

function App() {
  const [user, setUser] = useState({ name: "Steve" });
  return (
    <div>
      <Navbar />
      <MainPage user={user} />
    </div>
  );
}
export default App;

// Navbar Component
function Navbar() {
  return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>;
}

//MainPage Component
function MainPage({ user }) {
  return (
    <div>
      <h3>Main Page</h3>
      <Content user={user} />
    </div>
  );
}

// Content Component
function Content({ user }) {
  return (
    <div>
      <Message user={user} />
    </div>
  );
}

//Message Component
function Message({ user }) {
  return <p>Welcome {user.name}</p>;
}

请注意,我们没有把我们的组件分割成不同的文件,然后再导入每个单独的组件,而是把它们都放在同一个文件中,作为它们自己的单独的功能组件。现在我们可以使用它们而不需要任何外部导入。

我们的输出结果将是。

Our basic demo app view

现在我们有了一个基本的工作应用,让我们再一次通过解决道具钻探问题来比较这个解决方案,这次是使用Context API。

通过使用Context API解决道具钻取问题

对于那些不熟悉Context API的人来说,我们先快速了解一下它的作用。

Context API基本上可以让你通过用一个上下文提供者包装你的状态/数据来向多个组件广播。然后,它使用其值属性将该状态传递给上下文提供者。然后,子组件可以在需要时使用上下文消费者或useContext Hook接入该提供者,并访问由上下文提供者提供的状态。

让我们创建一个上下文并将user 对象传递给上下文提供者。然后我们将继续前进,用上下文提供者包装我们想要的组件,并在需要的特定组件中访问它所持有的状态。

import "./App.css";
import { createContext, useContext } from "react";

//Creating a context
const userContext = createContext();

function App() {
  return (
    <div>
      <Navbar />
      <userContext.Provider value={{ user: "Steve" }}>
        <MainPage />
      </userContext.Provider>
    </div>
  );
}
export default App;

function Navbar() {
  return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>;
}

function MainPage() {
  return (
    <div>
      <h3>Main Page</h3>
      <Content />
    </div>
  );
}

function Content() {
  return (
    <div>
      <Message />
    </div>
  );
}

function Message() {
// Getting access to the state provided by the context provider wrapper
  const { user } = useContext(userContext);
  return <p>Welcome {user} :)</p>;
}

我们首先导入一个createContext Hook,它用于创建一个上下文,还有一个useContext Hook,它将提取由上下文提供者提供的状态。

然后我们调用createContext Hook函数,它返回一个空值的上下文对象。然后将其存储在一个名为userContext 的变量中。

继续前进,我们继续用Context.Provider 包裹MainPage 组件,并将user 对象传递给它,它将提供给嵌套在MainPage 组件中的每个组件。

最后,我们在嵌套在MainPage 组件中的Message 组件中提取这个用户,使用useContext Hook和一些解构的方法。

我们已经完全取消了通过中间组件传递用户道具的需要。因此,我们已经解决了道具钻取的问题。

The rendered output of our app remains the same when using the Context API

我们的渲染输出保持不变,但下面的代码却更精简、更干净。

那么,为什么这是个问题呢?

严重依赖Context API的两个主要缺点

尽管我们通过将Context API引入我们的应用程序,完全解决了道具钻取的问题,但它也不是没有自己的注意事项,比如组件的可重用性和性能问题。

这些注意事项,虽然在小规模的应用中可以忽略不计,但同样会导致不想要的结果。Context的文档本身就对这些注意事项提出了警告。

在你使用Context之前
Context主要用于某些数据需要被不同嵌套级别的许多组件所访问。少用它,因为它使组件的重用更加困难。
如果你只想避免将一些道具传递到许多层,组件组合通常是一个比上下文更简单的解决方案。

组件重用性的问题

当一个上下文提供者被包裹在多个组件上时,我们隐含地将存储在该提供者中的任何状态或数据传递给它所包裹的子组件。

注意到我说的是隐性的吗?我们并没有真正地将状态传递给这些组件--直到我们启动一个实际的上下文消费者或useContext Hook--但我们已经隐含地使这些组件依赖于这个上下文提供者提供的状态。

问题来自于试图在我们的上下文提供者的范围之外重复使用这些组件。该组件首先尝试确认由上下文提供者提供的隐式状态在渲染前是否仍然存在。当它没有找到这个状态时,它就会抛出一个渲染错误。

还不清楚吗?想象一下我们之前的例子。假设我们想重用Message 组件,根据不同的条件显示不同的信息,而且这个Message 组件要放在上下文提供者包装器的边界之外。

import { createContext, useContext } from "react";
//Creating a context
const userContext = createContext();
function App() {
  return (
    <>
      <div>
        <Navbar />
        <userContext.Provider value={{ user: "Steve" }}>
          <MainPage />
        </userContext.Provider>
      </div>
      {/* Trying to use the message component outside the Context Provider*/}
      <Message />
    </>
  );
}
export default App;
function Navbar() {
  return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>;
}
function MainPage() {
  return (
    <div>
      <h3>Main Page</h3>
      <Content />
    </div>
  );
}
function Content() {
  return (
    <div>
      <Message />
    </div>
  );
}
function Message() {
  // Getting access to the state provided by the context provider wrapper
  const { user } = useContext(userContext);
  return <p>Welcome {user} :)</p>;
}

我们在上面的输出将是。

You'll receive a TypeError from the above code snippet

如上所见,任何试图这样做的行为也会导致渲染错误,因为Message 组件现在依赖于上下文提供者状态中的用户对象。试图将它伸向由上下文提供者提供的任何现有的user 对象将会失败。下面是上述片段的一个视觉图示。

Visualizing our render error

有些人建议通过用上下文包装整个应用程序来绕过这个问题。这对于较小的应用程序来说是没有问题的,但对于较大或较复杂的应用程序来说,这可能不是一个实用的解决方案,因为我们经常想在我们的应用程序中使用多个上下文提供者,这取决于需要管理的内容。

性能方面的问题

Context API使用一种比较算法,将其当前状态的值与它收到的任何更新进行比较,每当发生变化时,Context API就会将这种变化广播给消费其提供者的每个组件,这反过来又导致这些组件的重新渲染。

乍一看,这似乎是微不足道的,但当我们严重依赖Context进行基本的状态管理时,我们通过不必要地将我们所有的状态推送到上下文提供者中来过度设计我们的应用程序。正如你所期望的那样,当许多组件依赖于这个上下文提供者时,这不是很好的表现,因为只要状态有更新,它们就会重新渲染,而不管这个变化是否涉及或影响到它们。

介绍一下组件的组成

让我们回顾一下我们在这里已经看到的React创建者的一些建议。

如果你只想避免将一些道具通过很多层传递,那么组件组合往往是比上下文更简单的解决方案。

你可能会从我之前提到的React文档中认出这句话--确切地说,它是在Context API部分

较新的React开发者可能会想知道 "组件组合 "是什么意思。组件组合并不是一个新增加的功能,我敢说它是React和许多JavaScript框架的基本原则。

当我们构建React应用程序时,我们通过构建多个可重复使用的组件来实现,这些组件几乎可以被看作是独立的乐高积木。每个乐高积木(组件)都被认为是我们最终界面的一个部分--当它们被组装在一起时,就形成了我们应用程序的完整界面。

正是这种将组件作为乐高积木组装的过程,被称为组件组合。

如果你以前建过一个React应用程序(我相信你已经建过了),你可能已经使用了组件组合,但没有认识到它是什么:一种管理我们应用程序状态的替代方法。在这篇文章中,我们将主要关注两种类型的组件组合:容器组件专用组件

容器组件

和JavaScript中的一切一样(除了原始数据类型),React中的组件不过是对象,和典型的对象一样,组件可以包含不同种类的属性,包括其他组件。有两种方法可以实现这一壮举。

  1. 通过明确地将一个或多个组件传递给另一个组件,作为该组件的道具,然后可以在该组件中提取和呈现。
  2. 通过将一个或多个子组件包裹在父组件周围,然后使用默认的子组件道具来捕捉这些子组件。

让我们来看看第一种方式。

import {useState} from 'react'

function App() {
  const [data, setData] = useState("some state");
  return <ComponentOne ComponentTwo={<ComponentTwo data={data} />} />;
}

function ComponentOne({ ComponentTwo }) {
  return (
    <div>
      <p>This is Component1, it receives component2 as a prop and renders it</p>
      {ComponentTwo}
    </div>
  );
}

function ComponentTwo({ data }) {
  return <h3>This is Component two with the received state {data}</h3>;
}

与其在组件中嵌套组件,然后费力地通过道具钻取来传递数据,我们可以简单地将这些组件提升到我们的根应用程序中,然后手动将预定的子组件传递给父组件,并将预定的数据直接附加到子组件中。然后,父组件将把它渲染成一个道具。

现在,让我们来看看第二种方式。

function App() {
  const [data, setData] = useState("some state");

  return (
    <ParentComponent>
      <ComponentOne>
        <ComponentTwo data={data} />
      </ComponentOne>
    </ParentComponent>
  );
}

function ParentComponent({ children }) {
  return <div>{children}</div>;
}
function ComponentOne({ children }) {
  return (
    <>
      <p>This is Component1, it receives component2 as a child and renders it</p>
      {children}
    </>
  );
}

function ComponentTwo({ data }) {
  return <h3>This is Component two with the received {data}</h3>;
}

在这一点上,代码应该是不言自明的--每当我们将一个组件包裹在另一个组件上时,包裹的组件就成为被包裹的组件的父组件。然后,子组件可以在父组件中使用默认的children道具来接收,该道具负责渲染子组件。

专业化组件

专用组件是一种通用组件,通过传递符合特定变体条件的道具,有条件地创建渲染自身的专用变体。

这种形式的组件构成不一定能解决道具的钻研,而是更关注可重用性和创建更少的组件,当与容器组件混合在一起时,可以有效地在构成一个有状态的界面中发挥关键作用。

下面是一个专门的组件的例子,以及它是如何促进重用性的。

function App() {
  return (
    <PopupModal title="Welcome" message="A popup modal">
      <UniqueContent/>
    </PopupModal>
  );
}

function PopupModal({title, message, children}) {
  return (
    <div>
      <h1 className="title">{title}</h1>
      <p className="message">{message}</p>
      {children && children}
    </div>
  );
}

function UniqueContent() {
  return<div>Unique Markup</div>
}

为什么组件组成很重要

既然你对组件的组成有了一定的了解,要想知道组件的组成有多大的用处,应该不是什么火箭科学。下面列举几个原因。

  • 它鼓励我们组件的可重用性
  • 它很容易解决所谓的没有外部库的道具钻孔问题
  • 通过将我们的大部分组件提升到根层,并智能地结合各种组合方法,它可以成为状态管理的有效替代方法
  • 组成使你的代码更可预测,更容易调试
  • 它很容易增强与其他组件共享状态和功能的能力
  • 从根本上说,它是React构建界面的方式。

我可以继续讲述组件组合的各种重要方式,但你应该已经看到了它的模式。我们也会在下一节中逐一介绍,所以,继续。

使用组件组合重新创建我们的应用程序

让我们重构我们的应用程序以使用组件组合。我们将用两种方式来展示它的灵活性。

import { useState } from "react";

function App() {
  const [user, setState] = useState({ name: "Steve" });
  return (
    <div>
      <Navbar />
      <MainPage content={<Content message={<Message user={user} />} />} />
    </div>
  );
}
export default App;

function Navbar() {
  return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>;
}

function MainPage({ content }) {
  return (
    <div>
      <h3>Main Page</h3>
      {content}
    </div>
  );
}

function Content({ message }) {
  return <div>{message}</div>;
}

function Message({ user }) {
  return <p>Welcome {user.name} :)</p>;
}

function App() {
  const [user, setState] = useState({ name: "Steve" });
  return (
    <div>
      <Navbar />
      <MainPage>
        <Content>
          <Message user={user} />
        </Content>
      </MainPage>
    </div>
  );
}
export default App;

function Navbar() {
  return <nav style={{ background: "#10ADDE", color: "#fff" }}>Demo App</nav>;
}

function MainPage({ children }) {
  return (
    <div>
      <h3>Main Page</h3>
      {children}
    </div>
  );
}

function Content({ children }) {
  return <div>{children}</div>;
}

function Message({ user }) {
  return <p>Welcome {user.name} :)</p>;
}

从上面的两个片段中可以看出,有几种方法可以进行组件的组合。在第一个片段中,我们利用了React的props功能,将组件作为一个简单的对象传递给每个父级,并将数据附加到感兴趣的组件上。

在第二个片段中,我们利用了children 属性来创建我们的布局的纯组合,数据直接传递给感兴趣的组件。我们可以很容易地想出更多的方法来重构这个应用程序,但现在你应该清楚地看到只依靠组件组合来解决道具钻研的可能性。

总结

React提供了一个强大的组合模式,不仅可以管理组件,还可以管理我们应用中的状态。正如React的Context文档中所写的。

Context的设计是为了共享那些可以被认为是React组件树的 "全局 "数据,例如当前的认证用户、主题或首选语言。

我们经常建议你少依赖Context或其他库来进行本地状态管理,特别是如果是为了避免道具的钻研,组件的组成很容易成为你最好的选择。

参考文献

React文档。

  1. 组成与继承
  2. 上下文

The postA better way of solving prop drilling in React appsappeared first onLogRocket Blog.