专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  6 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  17 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  17 小时前  
前端早读课  ·  【第3451期】前端 TypeError ... ·  昨天  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
51好读  ›  专栏  ›  前端早读课

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

前端早读课  · 公众号  · 前端  · 2025-02-07 08:00

正文

前言

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 进行扩展







请到「今天看啥」查看全文