在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株式会社中,我们正在招募新成员。