首页   

【第3452期】React 开发中使用开闭原则

前端早读课  · 前端  · 1 月前

正文

前言

React 开发中应用开闭原则(Open-Closed Principle, OCP),通过组件的组合和高阶组件等方式,使得组件易于扩展而不需要修改现有代码,从而提高了代码的可维护性和灵活性。今日前端早读课文章由 @ikoofe 翻译,公号:KooFE 前端团队授权分享。

译文从这开始~~

开闭原则(Open-Closed Principle, OCP)指出,软件实体应该对扩展开放,但对修改关闭。在 React 中,这意味着:组件应该易于扩展,而不需要修改其现有代码。让我们看看这在实际中是如何体现的。

【第2677期】如何在React中应用SOLID原则?

封闭组件的问题

以下是一个常见的反模式:

 // 不要这样做
const Button = ({ label, onClick, variant }: ButtonProps) => {
let className = "button";

// 直接为每种 variant 修改
if (variant === "primary") {
className += " button-primary";
} else if (variant === "secondary") {
className += " button-secondary";
} else if (variant === "danger") {
className += " button-danger";
}

return (
<button className={className} onClick={onClick}>
{label}
button>
);
};

这违反了开闭原则,因为:

  • 添加新 variant 需要修改组件

  • 组件需要知道所有可能的 variant

  • 每次添加新 variant 都会使测试变得更加复杂

构建开放组件

让我们重构这个组件以遵循开闭原则:

 type ButtonBaseProps = {
label: string,
onClick: () => void,
className?: string,
children?: React.ReactNode,
};

const ButtonBase = ({
label,
onClick,
className = "",
children,
}: ButtonBaseProps) => (
<button className={`button ${className}`.trim()} onClick={onClick}>
{children || label}
button>
);

// variant 组件扩展基础组件
const PrimaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-primary" />
);

const SecondaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-secondary" />
);

const DangerButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-danger" />
);

现在我们可以轻松添加新 variant,而无需修改现有代码:

 // 添加新 variant 而不修改原始组件
const OutlineButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-outline" />
);

组件组合模式

让我们看一个更复杂的组合示例:

 type CardProps = {
title: string,
children: React.ReactNode,
renderHeader?: (title: string) => React.ReactNode,
renderFooter?: () => React.ReactNode,
className?: string,
};

const Card = ({
title,
children,
renderHeader,
renderFooter,
className = "",
}: CardProps) => (
<div className={`card ${className}` .trim()}>
{renderHeader ? (
renderHeader(title)
) : (
<div className="card-header">{title}div>
)}

<div className="card-content">{children}div>

{renderFooter && renderFooter()}
div>
);

// 无需修改即可扩展
const ProductCard = ({ product, onAddToCart, ...props }: ProductCardProps) => (
<Card
{...props}
renderFooter={() => (
<button onClick={onAddToCart}>Add to Cart - ${product.price}button>
)}
/>
);

高阶组件扩展

高阶组件(HOC)提供了另一种遵循开闭原则的方式:

【第3425期】JavaScript 高阶技巧

 type WithLoadingProps = {
isLoading?: boolean;
};

const withLoading = <P extends object>(
WrappedComponent: React.ComponentType<P>
) => {
return ({ isLoading, ...props }: P & WithLoadingProps) => {
if (isLoading) {
return <div className="loader">Loading...div>;
}

return <WrappedComponent {...props as P} />;
};
};

// 使用
const UserProfileWithLoading = withLoading(UserProfile);

遵循开闭原则的自定义 Hook

自定义 Hook 也可以遵循开闭原则:

 const useDataFetching = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetchData();
}, [url]);

const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};

return { data, error, loading, refetch: fetchData };
};

// 无需修改即可扩展
const useUserData = (userId: string) => {
const result = useDataFetching<User>(`/api/users/${userId}`);

// 添加用户特定功能
const updateUser = async (data: Partial<User>) => {
// 更新逻辑
};

return { ...result, updateUser };
};

测试优势

开闭原则使测试变得更加简单:

 describe("ButtonBase", () => {
it("renders with custom className", () => {
render(<ButtonBase label="Test" onClick={() => {}} className="custom" />);

expect(screen.getByRole("button")).toHaveClass("button custom");
});
});

// 新变体可以有各自的测试
describe("PrimaryButton", () => {
it("includes primary styling", () => {
render(<PrimaryButton label="Test" onClick={() => {}} />);

expect(screen.getByRole("button")).toHaveClass("button button-primary");
});
});

关键要点

  • 使用组合而非修改 —— 通过 props 和 render props 进行扩展

  • 创建易于扩展的基础组件

  • 利用高阶组件和自定义 Hook 实现可重用的扩展

  • 从扩展点的角度思考 —— 哪些部分可能需要变化?

  • 使用 TypeScript 确保扩展的类型安全

开闭原则与 “组合优于继承”

React 团队推荐的 “组合优于继承” 与开闭原则完美契合。以下是原因:

 // 基于继承的方法(灵活性较低)
class Button extends BaseButton {
render() {
return (
<button className={this.getButtonClass()}>
{this.props.icon && <Icon name={this.props.icon} />}
{this.props.label}
button>
);
}
}

// 基于组合的方法(更灵活,遵循开闭原则)
const Button = ({
label,
icon,
renderPrefix,
renderSuffix,
...props
}: ButtonProps
) => (
<ButtonBase {...props}>
{renderPrefix?.()}
{icon && <Icon name={icon} />}
{label}
{renderSuffix?.()}
ButtonBase>
);

// 现在我们可以无需修改即可扩展行为
const DropdownButton = ({ items, ...props }: DropdownButtonProps) => (
<Button
{...props}
renderSuffix={() => <DropdownIcon />}
onClick={() => setIsOpen(true)}
/>
);

const LoadingButton = ({ isLoading, ...props }: LoadingButtonProps) => (
<Button
{...props}
renderPrefix={() => isLoading && <Spinner />}
disabled={isLoading}
/>
);

这种基于组合的方法:

  • 使组件对扩展开放(通过 props 和 render 函数)

  • 保持基础组件对修改关闭

  • 允许无限的行为组合

  • 保持类型安全和 props 透明性

React 团队对组合的偏好不仅仅是风格问题,而是关于创建可扩展、可维护的组件,这些组件自然遵循开闭原则。

结论

开闭原则可能看起来有些抽象,但在 React 中,它转化为使我们的组件更易于维护和灵活的实际模式。结合 SOLID 原则,它有助于创建一个易于扩展和维护的健壮架构。

😀 每天只需花五分钟即可阅读到的技术资讯,加入【早阅】共学,可联系 vx:zhgb_f2er

关于本文
译者:@ikoofe
译文:https://mp.weixin.qq.com/s/W_HYlxKsy_T6j-OB_e8jfg
作者:@christianekrem
原文:
https://cekrem.github.io/posts/open-closed-principle-in-react/

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。

© 2024 精读
删除内容请联系邮箱 2879853325@qq.com