作为 prop 的 React 组件:正确的方式

码道人 2022-06-23 13:36:44 阅读数:402

react组件方式正确prop

在 React 中,有一百万种方法可以做完全相同的事情。例如,如果我需要将一个组件作为 prop 传递给另一个组件,我应该怎么做?如果在流行的开源库中搜索答案,我会发现:

那么哪种方法是最好的方法,又应该避免哪种方法呢?哪一个应该包含在“React 最佳实践”列表中,为什么?

为什么要将组件作为 prop 传递?

在开始编码之前,让我们首先了解为什么我们希望将组件作为 props 开始传递。简短的回答是:为了灵活性并简化这些组件之间的数据共享。

例如,想象一下,我们正在实现一个带有图标的按钮。当然,我们可以像这样实现它:

const Button = ({ children }: { children: ReactNode }) => {
return (
<button>
<SomeIcon size="small" color="red" />
{children}
</button>
);
};

但是如果我们需要让人们能够改变那个图标呢?我们可以为此引入iconName 这个 prop:

type Icons = 'cross' | 'warning' | ... // all the supported icons
const getIconFromName = (iconName: Icons) => {
switch (iconName) {
case 'cross':
return <CrossIcon size="small" color="red" />;
...
// all other supported icons
}
}
const Button = ({ children, iconName }: { children: ReactNode, iconName: Icons }) => {
const icon = getIconFromName(name);
return <button>
{icon}
{children}
</button>
}

人们改变图标外观的能力怎么样?例如改变它的大小和颜色?我们还必须为此引入一些 prop:

type Icons = 'cross' | 'warning' | ... // all the supported icons
type IconProps = {
size: 'small' | 'medium' | 'large',
color: string
};
const getIconFromName = (iconName: Icons, iconProps: IconProps) => {
switch (iconName) {
case 'cross':
return <CrossIcon {...iconProps} />;
...
// all other supported icons
}
}
const Button = ({ children, iconName, iconProps }: { children: ReactNode, iconName: Icons, iconProps: IconProps }) => {
const icon = getIconFromName(name, iconProps);
return <button>
{icon}
{children}
</button>
}

当按钮中的某些内容发生变化时,让人们能够更改图标怎么样?例如,如果一个按钮悬停,并且我想将图标的颜色更改为不同的颜色。我甚至不打算在这里实现它,它太复杂了:我们必须在每个父组件中引入状态管理,在按钮悬停时设置状态等等。

它不仅是一个非常有限和复杂的 API。我们还强制Button组件知道它可以渲染的每个图标,这意味着捆绑的Button不仅会包含它自己的代码,还会包含列表中的每个图标。

让我们看看如何使用在开始时确定的三种模式来完成它:

  • 作为元素传递
  • 作为组件传递
  • 作为函数传递

构建一个带有图标的按钮

或者,准确地说,让我们构建三个按钮,使用 3 个不同的 API 来传递图标,然后对它们进行比,看看哪个更好。对于图标,我们将使用Material ui components library中的图标之一。让我们从基础开始,首先构建 API。

第一:图标作为 React 元素

我们只需要将一个元素传递给icon按钮的 prop,然后像任何其他元素一样在子元素附近呈现该图标。

type ButtonProps = {
children: ReactNode;
icon: ReactElement<IconProps>;
};
export const ButtonWithIconElement = ({ children, icon }: ButtonProps) => {
return (
<button>
// our icon, same as children, is just React element
// which we can add directly to the render function
{icon}
{children}
</button>
);
};

然后可以像这样使用它:

<ButtonWithIconElement icon={<AccessAlarmIconGoogle />}>button here</ButtonWithIconElement>

二:图标作为组件

我们需要创建一个以大写字母开头的 prop 来表示它是一个组件,然后像任何其他组件一样从 props 渲染该组件。

type ButtonProps = {
children: ReactNode;
Icon: ComponentType<IconProps>;
};
export const ButtonWithIconComponent = ({ children, Icon }: ButtonProps) => {
return (
<button>
// our button is a component
// its name starts with a capital letter to signal that
// so we can just render it here as any other component
<Icon />
{children}
</button>
);
};

然后可以像这样使用它:

import AccessAlarmIconGoogle from '@mui/icons-material/AccessAlarm';
<ButtonWithIconComponent Icon={AccessAlarmIconGoogle}>button here</ButtonWithIconComponent>;

第三:图标作为函数

我们需要创建一个以 render 开头的 prop 来表明它是一个渲染函数,即一个返回元素的函数,在按钮内调用该函数并将结果添加到组件的渲染函数中,就像任何其他元素一样。

type ButtonProps = {
children: ReactNode;
renderIcon: () => ReactElement<IconProps>;
};
export const ButtonWithIconRenderFunc = ({ children, renderIcon }: ButtonProps) => {
// getting the Element from the function
const icon = renderIcon();
return (
<button>
// adding element like any other element here
{icon}
{children}
</button>
);
};

然后像这样使用它:

<ButtonWithIconRenderFunc renderIcon={() => <AccessAlarmIconGoogle />}>button here</ButtonWithIconRenderFunc>

是时候对这些 API 进行测试了。

修改图标的大小和颜色

我们先看看能否在不影响按钮的情况下,根据自己的需要调整我们的图标。

一:图标作为 React 元素

再简单不过了:我们所需要的只是将一些 prop 传递给图标:

<ButtonWithIconElement icon={<AccessAlarmIconGoogle fontSize="small" color="warning" />}>button here</ButtonWithIconElement>

二:图标作为组件

也很简单:我们需要将我们的图标提取到一个组件中,并在返回元素中传递 prop。

const AccessAlarmIcon = () => <AccessAlarmIconGoogle fontSize="small" color="error" />;
const Page = () => {
return <ButtonWithIconComponent Icon={AccessAlarmIcon}>button here</ButtonWithIconComponent>;
};

重要AccessAlarmIcon组件应该始终在组件之外定义,否则它会在每次重新渲染Page时重新创建这个组件,这对性能非常不利并且容易出现错误。

三:图标作为函数

与第一个几乎相同:只需将 prop 传递给元素。

<ButtonWithIconRenderFunc
renderIcon={() => (
<AccessAlarmIconGoogle fontSize="small" color="success" />
)}
>

这三个都很容易完成,我们有无限的灵活性来修改它们,并且不需要为单一的事情影响按钮。

按钮中图标大小的默认值

你可能已经注意到,我对所有三个示例都使用了相同的图标大小。当实现一个通用按钮组件时,很可能你也会有一些控制按钮大小的道具。无限灵活性很好,但对于设计系统,你需要一些预定义类型的按钮。对于不同的按钮大小,你希望按钮控制图标的大小,而不是将其留给消费者,这样就不会在大按钮中出现小图标,反之亦然。

那么,按钮是否可以控制图标的一个方面,同时保持灵活性不变?

一:图标作为 React 元素

我们已经收到图标作为预定义元素,所以我们唯一能做的就是使用React.cloneElementapi 克隆该元素并覆盖它的一些 prop:

// in the button component
const clonedIcon = React.cloneElement(icon, { fontSize: 'small' });
return (
<button>
{clonedIcon}
{children}
</button>
);

但是默认值怎么办呢?如果我希望消费者能够在需要时更改图标的大小怎么办?

仍然有可能,但是代码会更丑陋,只需要从元素中提取传递的 prop 并将它们作为默认值:

const clonedIcon = React.cloneElement(icon, {
fontSize: icon.props.fontSize || 'small',
});

从消费者方面来看,一切都和以前一样

<ButtonWithIconElement icon={<AccessAlarmIconGoogle color="warning" fontSize="large" />} />

二:图标作为组件

这里更有趣。首先,我们需要为按钮侧的图标赋予默认值:

export const ButtonWithIconComponent = ({ children, Icon }: ButtonProps) => {
return (
<button>
<Icon fontSize="small" />
{children}
</button>
);
};

当我们通过直接导入的图标时,这将完美地工作:

import AccessAlarmIconGoogle from '@mui/icons-material/AccessAlarm';
<ButtonWithIconComponent Icon={AccessAlarmIconGoogle}>button here</ButtonWithIconComponent>;

Iconprop 在这里只不过是对 UI 图标组件的引用,并且知道如何处理这些 prop。但是当我们必须向它传递一些颜色时,我们将这个图标提取到一个组件中,记得吗?

const AccessAlarmIcon = () => <AccessAlarmIconGoogle fontSize="small" color="error" />;

现在Icon是对该包装组件的引用,它只是假设它没有任何 props。如果你以前从未使用过它,这整个模式可能会令人困惑。

为了修复图标,我们只需要将AccessAlarmIcon接收到的 prop 传递给实际的图标。通常,它是通过传解构完成的:

const AccessAlarmIcon = (props) => <AccessAlarmIconGoogle {...props} color="error" />;

或者也可以手工挑选:

const AccessAlarmIcon = (props) => <AccessAlarmIconGoogle fontSize={props.fontSize} color="error" />;

虽然这种模式看起来很复杂,但它实际上给了我们完美的灵活性:按钮可以轻松设置自己的道具,消费者可以选择是否要按照按钮给出的方向以及他们想要多少。例如,如果我想覆盖按钮的值并设置我自己的图标大小,我需要做的就是忽略来自按钮的 prop:

const AccessAlarmIcon = (props) => (
// just ignore all the props coming from the button here
// and override with our own values
<AccessAlarmIconGoogle fontSize="large" color="error" />
);

三:图标作为函数

这与将图标作为组件几乎相同,只是使用的是函数。首先,调整按钮以将设置传递给renderIcon函数:

const icon = renderIcon({
fontSize: 'small',
});

然后在消费者端,类似于 Component 步骤中的 props,将该设置传递给渲染的组件:

<ButtonWithIconRenderFunc renderIcon={(settings) => <AccessAlarmIconGoogle fontSize={settings.fontSize} color="success" />}>
button here
</ButtonWithIconRenderFunc>

同样,如果我们想覆盖大小,我们需要做的就是忽略设置并传递我们自己的值:

<ButtonWithIconRenderFunc
// ignore the setting here and write our own fontSize
renderIcon={(settings) => <AccessAlarmIconGoogle fontSize="large" color="success" />}
>
button here
</ButtonWithIconRenderFunc>

当按钮悬停时更改图标

现在应该决定一切的最终测试:我想让用户能够在按钮悬停时修改图标。

首先,让我们教按钮注意悬停。只需一些状态和回调来设置该状态就可以了:

export const ButtonWithIcon = (...) => {
const [isHovered, setIsHovered] = useState(false);
return (
<button
onMouseOver={() => setIsHovered(true)}
onMouseOut={() => setIsHovered(false)}
>
...
</button>
);
};

然后是图标。

一:图标作为 React 元素

这是最有趣的。首先,我们需要将isHover从按钮传递给图标:

const clonedIcon = React.cloneElement(icon, {
fontSize: icon.props.fontSize || 'small',
isHovered: isHovered,
});

现在,有趣的是,我们创建了与实现“图标即组件”时完全相同的思维圈。我们将isHover属性传递给图标组件,现在我们需要转到消费者,将原始图标组件包装到另一个组件中,该组件将具有isHover来自按钮的 prop,它应该返回我们要在按钮中呈现的图标,而不是原来简单的直接渲染图标:

<ButtonWithIconElement icon={<AccessAlarmIconGoogle color="warning" />}>button here</ButtonWithIconElement>

我们应该创建一个包装器组件,该组件在其 props 中具有并呈现该图标作为结果:

const AlarmIconWithHoverForElement = (props) => {
return (
<AccessAlarmIconGoogle
// don't forget to spread all the props!
// otherwise you'll lose all the defaults the button is setting
{...props}
// and just override the color based on the value of `isHover`
color={props.isHovered ? 'primary' : 'warning'}
/>
);
};

然后在按钮本身中渲染该新组件。

二:图标作为组件

首先,将 isHover 传递给按钮中的图标:

<Icon fontSize="small" isHovered={isHovered} />

然后返回给消费者:

<ButtonWithIconComponent Icon={AlarmIconWithHoverForElement}>button here</ButtonWithIconComponent>

完美运行。

三:图标作为函数

同样,只需将isHovered值作为参数传递给函数:

const icon = renderIcon({
fontSize: 'small',
isHovered: isHovered,
});

然后在消费者端使用它:

<ButtonWithIconRenderFunc
renderIcon={(settings) => (
<AccessAlarmIconGoogle
fontSize={settings.fontSize}
color={settings.isHovered ? "primary" : "warning"}
/>
)}

完美运行。

总结与答案:应该使用哪种方式?

如果你阅读了完整的文章,你现在可能会说:这些方式不是一回事吗?有什么不同?

是的,没有正确的答案。所有这些都或多或少相同,你可能可以在任何地方只使用一种模式来实现 99% 的所需用例(如果不是 100%)。这里唯一的区别是语义,哪个领域最复杂,以及个人偏好。

原文:https://adevnadia.medium.com/react-component-as-prop-the-right-way-%EF%B8%8F-949fce84088d

版权声明:本文为[码道人]所创,转载请带上原文链接,感谢。 https://markdowner.net/v1/article/by_user/327436058096582656