在React中,意识到Props Drilling问题的组件设计

首先

你好,我是大倉,從事前端工程師的工作。

在这篇文章中,我将重点介绍我个人感兴趣的React的Props Drilling问题,并介绍了学习如何通过高效的组件设计来解决这个问题。

本文是HRBrain Advent Calendar 2023的第6天的文章。

 

什么是”Prop Drilling”问题?

属性钻取(Prop Drilling)是在React组件结构中,通过层层传递数据(Props)从父组件传递到子孙组件的过程。

React应用程序由许多组件组成,这些组件形成了类似树形的层次结构。React有一个基本原则,即为了简化应用程序的状态管理并容易追踪数据流,”数据总是从父组件流向子组件”。因此,在具有多个层次的组件树中,基本上不能直接将数据传递给下级组件(跳过中间组件)。

Prop Drilling的问题是,每个中间组件都需要将数据传递给下层组件,即使它们并不直接使用该数据。
在具有多层次的大型应用程序中,这可能会变得非常混乱。尽管中间组件实际上并不需要这些数据(Props),但它们需要扮演传递数据的角色,从而导致代码复杂化和可重用性降低。

为了解决这个问题,需要优化组件间的数据流。接下来,我们将通过与典型的Prop Drilling方法进行比较,了解朝着更高效且易于维护的组件设计的具体方法。

选项1:上下文 API

通过使用React的Context API,您可以在组件树的特定级别提供数据,并在需要的组件中使用所需的数据。

典型的例子是Prop Drilling。

import React from "react";

type GrandChildProps = {
  value: string;
};

const GrandChildComponent = ({ value }: GrandChildProps) => (
  <div>{`Value is: ${value}`}</div>
);

type ChildProps = {
  value: string;
};

const ChildComponent = ({ value }: ChildProps) => (
  <GrandChildComponent value={value} />
);

const App = () => {
  const value = "Hello World";

  return <ChildComponent value={value} />;
};

ChildComponent的作用是将value传递给GrandChildComponent作为“中转”。这样一来,ChildComponent就依赖于特定的props(value),导致其重用性降低。
而且,如果组件的嵌套更深,就必须经过更多的中间组件,增加了代码的复杂性。

使用Context API的示例

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

type MyContextType = {
  value: string;
};

const MyContext = createContext<MyContextType>({ value: '' });

const App = () => {
  const value = "Hello World";

  return (
    <MyContext.Provider value={{ value }}>
      <ChildComponent />
    </MyContext.Provider>
  );
};

const GrandChildComponent = () => {
  const { value } = useContext(MyContext);

  return <div>{`Value is: ${value}`}</div>;
};

const ChildComponent = () => (
  <GrandChildComponent />
);

通过这种方法,可以在不经过ChildComponent的情况下将value传递给GrandChildComponent。此外,每个组件不需要依赖特定的Props,从而提高了可重用性。
数据从App流向GrandChildComponent是通过Context进行的,这样可以使代码结构更加清晰和易于维护。

公式文档的建议事项

在考虑使用 Context API 时需要注意。例如,如果 Context 的值经常变化,这些变化会传播到整个组件树中,可能对性能产生负面影响。根据 React 的官方文档,建议在使用 Context API 之前,首先要重新审视和重新设计组件结构。

请参考React的官方文档,详细了解更多信息。

 

方法二:组件合成

在需要不同布局和样式的页面和组件的情况下,组件合成非常有效。例如,在不同的部分中使用共同的布局组件,并插入不同的内容,通过避免代码重复,可以提高可重用性。

典型的的Prop Drilling例子

import React from "react";

type LayoutProps = {
  title: string;
  isDarkMode: boolean;
  children: React.ReactNode;
}

const Header = ({ title, isDarkMode }: { title: string; isDarkMode: boolean }) => {
  return (
    <header style={{ backgroundColor: isDarkMode ? 'black' : 'white' }}>
      <h1>{title}</h1>
    </header>
  );
};

const Footer = ({ isDarkMode }: { isDarkMode: boolean }) => {
  return (
    <footer style={{ backgroundColor: isDarkMode ? 'black' : 'white' }}>
      <p>© 2023 My Website</p>
    </footer>
  );
};

const Layout = ({ title, isDarkMode, children }: LayoutProps) => {
  return (
    <div>
      <Header title={title} isDarkMode={isDarkMode} />
      <main>{children}</main>
      <Footer isDarkMode={isDarkMode} />
    </div>
  );
};

const App = () => {
  return (
    <Layout title="My App" isDarkMode={true}>
      <p>Welcome to my app!</p>
    </Layout>
  );
};

布局组件向页头和页脚分别传递属性(标题、是否为暗黑模式)。这样一来,会遇到以下问题。

    • HeaderとFooterはLayout コンポーネントに依存しているため、他の場所で再利用する際に不便である。

titleやisDarkModeが変更されるたびに、LayoutだけでなくHeaderとFooterも変更する必要がある。

使用了组件合成(Composition)的示例。

import React from "react";

type LayoutProps = {
  header: React.ReactNode;
  footer: React.ReactNode;
  children: React.ReactNode;
}

const Header = ({ children }: { children: React.ReactNode }) => {
  return <header>{children}</header>;
};

const Footer = ({ children }: { children: React.ReactNode }) => {
  return <footer>{children}</footer>;
};

const Layout = ({ header, footer, children }: LayoutProps) => {
  return (
    <div>
      {header}
      <main>{children}</main>
      {footer}
    </div>
  );
};

const App = () => {
  const isDarkMode = true;
  return (
    <Layout 
      header={<Header><h1 style={{ backgroundColor: isDarkMode ? 'black' : 'white' }}>My App</h1></Header>}
      footer={<Footer><p style={{ backgroundColor: isDarkMode ? 'black' : 'white' }}>© 2023 My Website</p></Footer>}
    >
      <p>Welcome to my app!</p>
    </Layout>
  );
};

Layout组件不直接控制子组件,而是为它们提供绘制的“位置”。这样可以减少组件之间的耦合,并使其成为一个通用的布局组件。

Layout组件可以接收Header和Footer作为ReactNode的属性,从而具有以下优点。

    • HeaderとFooterはLayoutから独立しているため、他のコンポーネント内で自由に再利用できる。

 

    Layoutコンポーネントは、任意のHeaderやFooterを受け入れることができるため、異なるスタイルや内容を持つHeaderやFooterを柔軟に取り入れることができる。

方法三:高阶组件(HOC)

高级组件(HOC)是一种函数,用于为组件添加功能,而不是直接将数据传递给子组件,而是生成一个带有所需数据的新组件。
特别适用于在不同的组件之间重复使用共享功能和逻辑。例如,通过使用高级组件(HOC)将共享逻辑,如认证、数据获取、错误处理等封装起来,可以将这些功能应用于多个需要它们的组件。

在这里,我们将以认证功能为例进行讨论。

典型的例的「Prop Drilling」。

import React from "react";

type User = {
  name: string;
  isAuthenticated: boolean;
}

const Profile = ({ user }: { user: User }) => {
  if (!user.isAuthenticated) {
    return <p>Please log in.</p>;
  }
  return <div>Welcome, {user.name}!</div>;
};

const Navbar = ({ user }: { user: User }) => {
  return (
    <nav>
      <Profile user={user} />
    </nav>
  );
};

const App = () => {
  const user = {
    name: "John Doe",
    isAuthenticated: true
  };

  return <Navbar user={user} />;
};

使用高阶组件(HOC)的示例

import React from "react";

type User = {
  name: string;
  isAuthenticated: boolean;
}

type WithAuthenticationProps = {
  user: User;
}

// HOC: 認証状態に基づいてコンポーネントをレンダリングする
const withAuthentication = <P extends object>(
  Component: React.ComponentType<P & WithAuthenticationProps>
) => {
  return (props: P) => {
    const user: User = {
      name: "John Doe",
      isAuthenticated: true, // 認証状態を設定
    };

    if (!user.isAuthenticated) {
      return <p>Please log in.</p>;
    }

    return <Component {...(props as P)} user={user} />;
  };
};

const Profile = withAuthentication(({ user }: WithAuthenticationProps) => {
  return <div>Welcome, {user.name}!</div>;
});

const Navbar = () => {
  return (
    <nav>
      <Profile />
    </nav>
  );
};

const App = () => {
  return <Navbar />;
};


在这个例子中,withAuthentication负责管理用户信息,并且只对经过认证的用户渲染Profile组件。这样一来,App组件就不需要直接向子组件传递用户信息了。

由于用户信息的管理和传递是在HOC内部进行的,因此每个组件都可以从数据管理中解放出来。

这种方法可以避免重复的认证逻辑,并提高组件的可重用性。此外,将应用程序的认证逻辑集中在一处,可以提高其可维护性和可扩展性。

最后

在本篇文章中,我们介绍了几种解决Props Drilling问题的方法,包括Context API、组件合成和高阶组件(HOC)。然而,这些方法各自都有优点和缺点,因此在选择最佳方法时需要注意项目特定的需求和结构不同。

另外,迄今为止我们只讨论了Props Drilling的问题,但这并不意味着Props Drilling本身就是一定不好的。相反,在小规模项目或简单组件结构中,Props Drilling可能是一种直观且简单的方法。因此,我认为与其突然引入Context API或其他复杂的技术手段,还不如根据项目的发展逐步进行重构更好。

请参考

 

以下是对原文的中文同义改写:

公关

在HRBrain株式会社中,我们正在招募新成员。

 

广告
将在 10 秒后关闭
bannerAds