反应-多态类型
关于
反应-多态类型 是允许在React中创建零运行时多态组件定义的库.
解释
什么是反应多态成分?
在 React 中构建设计系统或重新使用 UI 组件时,您可能会发现需要 多态组件。一个多态组件是一个多能组件,可以使基于道具的不同的 HTML 基本元素或自定义组件.
反应多态构件为消费者提供了灵活性,使他们能够指定使用道具制造的所希望的元素或构件类型.
例如,让我们考虑一个多形态标题组件:
import { createElement, ElementType, PropsWithChildren, ComponentProps } from 'react';
// Define the props for the polymorphic heading component
type HeadingProps<T extends ElementType = 'h1'> = PropsWithChildren<
{
as?: T;
} & ComponentProps<T>
>;
// Define the polymorphic heading component
export const Heading = <T extends ElementType = 'h1'>({ as = 'h1', children, ...rest }: HeadingProps<T>) =>
createElement(as, rest, children);
在上述例子中 Heading 组件可以使不同的标题级别(h1, (中文(简体) ), h2, (中文(简体) ), h3等)),基于: as 道具 (英语). 默认情况下,它使 h1 键.
你可以用这个 Heading 应用程序中的组件 :
const App = () => (
<article>
<Heading>My Main Headline</Heading>
<Heading as='h2'>A Subtitle</Heading>
<p>A description</p>
</article>
);
在这种情况下,同样的 Heading 组件用于制取两个不同的语义标记, h1 财务报告和审定财务报表 h2,允许您控制标题级别并保持应用程序的一致性.
多态构件为React中构建可弹性和可再使用的UI构件提供了优雅的解决方案,使您能够创建一个具有一致语义的一致设计系统.
这个包能解决什么问题?
使用 as 属性在给您提供的标记添加限制时或使用第三方组件时会变得复杂。 宣告每个组件的多态类型也可以是您想要抽象的乏味的任务.
与 @axa-ch/react-polymorphic-types,可以轻松地在多态反应组件中添加约束并避免冗余类型定义.
安装和使用
通过 npm 安装类型脚本 :
npm i @axa-ch/react-polymorphic-types -D
多态构件
以下配方为创建多形态成分提供了一个起点. 您可按规定复制并修改.
基本示例
这个例子展示了一个简单的多态标题元素. 它允许您独立定义其大小并使用道具进行标记.
import { ComponentPropsWithoutRef, createElement, ElementType } from 'react';
import { PolymorphicProps } from '@axa-ch/react-polymorphic-types';
// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;
// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
size?: HeadingSizes;
};
// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements = typeof HeadingDefaultElement> = PolymorphicProps<
HeadingOwnProps<T>,
T,
HeadingAllowedElements
>;
export const Heading = <T extends HeadingAllowedElements>({
as = HeadingDefaultElement,
size,
className,
children,
...rest
}: HeadingProps<T>) =>
createElement(
as,
{
...rest,
className: `${className} size-${size || 1}`,
},
children,
);
你可以用这个 Heading 应用程序中的组件如下:
const App = () => (
<article>
<Heading
as='h1'
size={2}
>
My Main Headline
</Heading>
<Heading
as='h2'
size={5}
>
A Subtitle
</Heading>
{/* The following component will throw a TypeScript error because 'div' elements are not allowed here */}
<Heading
as='div'
size={5}
>
A Subtitle
</Heading>
<p>A description</p>
</article>
);
参考文献的基本示例
这个例子与前作相似,但也允许使用React refs.
import { ComponentPropsWithoutRef, createElement, ElementType, forwardRef } from 'react';
import { PolymorphicProps, PolymorphicForwardedRef } from '@axa-ch/react-polymorphic-types';
// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;
// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
size?: HeadingSizes;
};
// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps<
HeadingOwnProps<T>,
T,
HeadingAllowedElements
>;
const HeadingInner = <T extends HeadingAllowedElements>(
{ as = HeadingDefaultElement, size, className, children, ...rest }: HeaadingProps<T>,
// notice the use of the PolymorphicForwardedRef type here
ref: PolymorphicForwardedRef<T>,
) =>
createElement(
as,
{
...rest,
ref,
className: `${className} size-${size || 1}`,
},
children,
);
// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Heading = forwardRef(HeadingInner) as unknown as <T extends HeadingAllowedElements>(
props: HeadingProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof HeadingInner>;
使用 @axa-ch/react-polymorphic-types 类型可以自动推断合适的 DOM 节点.
const App = () => {
// The use of HTMLHeadingElement type is safe
const ref = useRef<HTMLHeadingElement | null>(null);
return (
<Heading
ref={ref}
as='h2'
/>
);
};
与 Ref 一起反应 19 示例
这个例子与前一个相似,但与React 19 the 调试员转发起来容易多了
import { ComponentPropsWithoutRef, createElement, ElementType, forwardRef } from 'react';
import { PolymorphicProps, PolymorphicForwardedRef } from '@axa-ch/react-polymorphic-types';
// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;
// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
size?: HeadingSizes;
ref?: PolymorphicForwardedRef<T>;
};
// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps<
HeadingOwnProps<T>,
T,
HeadingAllowedElements
>;
const Heading = <T extends HeadingAllowedElements>({
as = HeadingDefaultElement,
size,
className,
ref,
children,
...rest
}: HeadingProps<T>) =>
createElement(
as,
{
...rest,
ref,
className: `${className} size-${size || 1}`,
},
children,
);
使用 @axa-ch/react-polymorphic-types 类型可以自动推断合适的 DOM 节点.
const App = () => {
// The use of HTMLHeadingElement type is safe
const ref = useRef<HTMLHeadingElement | null>(null);
return (
<Heading
ref={ref}
as='h2'
/>
);
};
带有备忘录的基本示例
这个例子显示了使用React.memo的多态成分.
import { ComponentPropsWithoutRef, createElement, ElementType, memo } from 'react';
import { PolymorphicProps } from '@axa-ch/react-polymorphic-types';
// Default HTML element if the "as" prop is not provided
export const HeadingDefaultElement: ElementType = 'h1';
// List of allowed HTML elements that can be passed via the "as" prop
export type HeadingAllowedElements = typeof HeadingDefaultElement | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingSizes = 1 | 2 | 3 | 4 | 5 | 6;
// Component-specific props
export type HeadingOwnProps<T extends HeadingAllowedElements> = ComponentPropsWithoutRef<T> & {
size?: HeadingSizes;
};
// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type HeadingProps<T extends HeadingAllowedElements> = PolymorphicProps<
HeadingOwnProps<T>,
T,
HeadingAllowedElements
>;
const HeadingInner = <T extends HeadingAllowedElements>({
as = HeadingDefaultElement,
size,
className,
children,
...rest
}: HeadingProps<T>) =>
createElement(
as,
{
...rest,
className: `${className} size-${size || 1}`,
},
children,
);
// Memo with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Heading = memo(HeadingInner) as <T extends HeadingAllowedElements>(
props: HeadingProps<T>,
) => ReturnType<typeof HeadingInner>;
上述部分可在不增加间接费用的情况下消耗如下:
const App = () => (
<>
<Heading as='h2' />
</>
);
Exotic 组件示例
多态异域组件允许您为 HTML 使用 DOM 节点或自定义渲染功能 .
import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent } from 'react';
import { PolymorphicExoticProps, PolymorphicProps } from '@axa-ch/react-polymorphic-types';
// Default HTML element if the "as" prop is not provided
export const ContainerDefaultElement: ElementType = 'div';
// List of allowed HTML elements that can be passed via the "as" prop
export type ContainerAllowedDOMElements = typeof ContainerDefaultElement | 'article' | 'section';
export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent;
// Component-specific props
export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>;
// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements
? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements>
: PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>;
export const Container = <T extends ContainerAllowedElements>({
as = ContainerDefaultElement,
className,
children,
...rest
}: ContainerProps<T>) =>
createElement(
as,
{
...rest,
className,
},
children,
);
以上组件与直通HTML节点或外部异域组件相配合,例如由HTML节点提供的组件 帧移动。 。 。 .
import { motion } from 'framer-motion';
const App = () => (
<>
<Container as='div' />
{/* Notice that the exotic props here will be automatically inferred */}
<Container
as={motion.article}
layout
/>
</>
);
带有参考文献的 Exotic 组件示例
使用 refs 的多态异域组件略为复杂,需要一些额外的代码才能正常工作.
import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent, forwardRef } from 'react';
import { PolymorphicProps, PolymorphicForwardedRef, PolymorphicExoticProps } from '@axa-ch/react-polymorphic-types';
// Default HTML element if the "as" prop is not provided
export const ContainerDefaultElement: ElementType = 'div';
// List of allowed HTML elements that can be passed via the "as" prop
export type ContainerAllowedDOMElements = 'div' | 'article' | 'section';
export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent;
// Component-specific props
export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>;
// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements
? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements>
: PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>;
// Forwarded ref component
const ContainerInner = <T extends ContainerAllowedElements>(
{ as = ContainerDefaultElement, className, children, ...rest }: ContainerProps<T>,
ref: PolymorphicForwardedRef<T>,
) =>
createElement(
as,
{
...rest,
ref,
className,
},
children,
);
// Forward refs with generics is tricky
// see also https://fettblog.eu/typescript-react-generic-forward-refs/
export const Container = forwardRef<ContainerAllowedElements>(ContainerInner) as <T extends ContainerAllowedElements>(
props: ContainerProps<T> & { ref?: PolymorphicForwardedRef<T> },
) => ReturnType<typeof ContainerInner>;
根据上述例子,DOM节点将自动被推断出,包括在使用第三方异地渲染功能时.
import { motion } from 'framer-motion';
const App = () => {
const div = useRef<HTMLDivElement | null>(null);
// Article and other HTML5 tags are just of type HTMLElement
const article = useRef<HTMLElement | null>(null);
return (
<>
<Container
ref={div}
as='div'
/>
<Container
ref={article}
as={motion.article}
layout
/>
</>
);
};
复杂的外观/功能组件示例
这个例子结合了您组件的多渲染策略,使其消费者有最大的灵活性.
// We need to infer the functional component properties so 'any' is used in this case
// You can also add strict types for your functional components, but it will reduce flexibility
import { ComponentPropsWithoutRef, createElement, ElementType, ExoticComponent, FC } from 'react';
import { PolymorphicFunctionalProps, PolymorphicExoticProps, PolymorphicProps } from '@axa-ch/react-polymorphic-types';
// Default HTML element if the "as" prop is not provided
export const ContainerDefaultElement: ElementType = 'div';
// List of allowed HTML elements that can be passed via the "as" prop
export type ContainerAllowedDOMElements = 'div' | 'article' | 'section';
export type ContainerAllowedElements = ContainerAllowedDOMElements | ExoticComponent | FC<any>;
// Component-specific props
export type ContainerOwnProps<T extends ContainerAllowedDOMElements> = ComponentPropsWithoutRef<T>;
// Extend own props with others inherited from the underlying element type
// Own props take precedence over the inherited ones
export type ContainerProps<T extends ContainerAllowedElements> = T extends ContainerAllowedDOMElements
? PolymorphicProps<ContainerOwnProps<T>, T, ContainerAllowedDOMElements>
: T extends FC<any>
? PolymorphicFunctionalProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>
: PolymorphicExoticProps<ContainerOwnProps<ContainerAllowedDOMElements>, T, ContainerAllowedDOMElements>;
export const Container = <T extends ContainerAllowedElements>({
as = ContainerDefaultElement,
className,
children,
...rest
}: ContainerProps<T>) =>
createElement(
as,
{
...rest,
className,
},
children,
);
让我们看看如何使用上面的组件 及其所有可能的渲染选项:
import { motion } from 'framer-motion';
type FooProps = ComponentPropsWithoutRef<'div'> & { size: 'small' | 'large'; name: string };
const Foo: FC<FooProps> = ({ className, size = 'large', ...rest }) => (
<div
{...rest}
className={`${className} the-foo ${size}`}
/>
);
const App = () => (
<>
<Container as='div' />
<Container
size='small'
name='foo'
as={Foo}
/>
<Container
as={motion.div}
layout
animate
/>
</>
);
贷项
没有这个工程就不会存在 反应-多态类型