在 TypeScript 中使用带有功能组件的点表示法

IT技术 javascript reactjs typescript
2021-03-30 03:04:09

官方 ReactJs 文档建议按照点符号创建组件,React-bootstrap库:

<Card>
  <Card.Body>
    <Card.Title>Card Title</Card.Title>
    <Card.Text>
      Some quick example text to build on the card title and make up the bulk of
      the card's content.
    </Card.Text>
  </Card.Body>
</Card>

感谢这个问题,我知道我可以使用功能组件来创建这个结构,就像在 javascript 中一样:

const Card = ({ children }) => <>{children}</>
const Body = () => <>Body</>

Card.Body = Body

export default Card

我决定使用 TypeScript 添加相应的类型:

const Card: React.FunctionComponent = ({ children }): JSX.Element => <>{children}</>
const Body: React.FunctionComponent = (): JSX.Element => <>Body</>

Card.Body = Body  // <- Error: Property 'Body' does not exist on type 'FunctionComponent<{}>'

export default Card

现在的问题是 TypeScript 不允许分配Card.Body = Body并给我错误:

类型“FunctionComponent<{}>”上不存在属性“Body”

那么我怎样才能正确地输入这个以使用这个代码结构呢?

4个回答
const Card: React.FunctionComponent & { Body: React.FunctionComponent } = ({ children }): JSX.Element => <>{children}</>
const Body: React.FunctionComponent = (): JSX.Element => <>Body</>

Card.Body = Body;

或更易读:

type BodyComponent = React.FunctionComponent;
type CardComponent = React.FunctionComponent & { Body: BodyComponent };

const Card: CardComponent = ({ children }): JSX.Element => <>{children}</>;
const Body: BodyComponent = (): JSX.Element => <>Body</>;

Card.Body = Body;

在花了很多时间弄清楚如何在forwardRef组件中使用点表示法之后,这是我的实现:

卡体组件:

export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(({ children, ...rest }, ref) => (
    <div {...rest} ref={ref}>
        {children}
    </div>
));

//Not necessary if Bonus feature wont be implemented 
CardBody.displayName = "CardBody";

卡片组件:

interface CardComponent extends React.ForwardRefExoticComponent<CardProps & React.RefAttributes<HTMLDivElement>> {
    Body: React.ForwardRefExoticComponent<CardBodyProps & React.RefAttributes<HTMLDivElement>>;
}

const Card = forwardRef<HTMLDivElement, CardProps>(({ children, ...rest }, ref) => (
    <div {...rest} ref={ref}>
        {children}
    </div>
)) as CardComponent;

Card.Body = CardBody;

export default Card;

在你的代码中使用它看起来像这样:

<Card ref={cardRef}>
    <Card.Body ref={bodyRef}>
        Some random body text
    </Card.Body>
</Card>

🚀 奖励功能🚀

如果你想要一个特定的订单:

...CardComponentInterface

const Card = forwardRef<HTMLDivElement, CardProps>(({ children, ...rest }, ref) => {

    const body: JSX.Element[] = React.Children.map(children, (child: JSX.Element) =>
        child.type?.displayName === "CardBody" ? child : null
    );

    return(
        <div {...rest} ref={ref}>
           {body}
        </div>
    )
}) as CardComponent;

...Export CardComponent

!!!如果children不存在,您将在尝试添加 CardBody 组件之外的任何其他内容时收到错误消息。这个用例非常具体,但有时可能很有用。

您当然可以继续添加组件(页眉、页脚、图像等)

我找到了一种巧妙的方法Object.assign来使点符号与 ts 一起使用。有类似的用例

type TableCompositionType = {
    Head: TableHeadComponentType;
    Body: TableBodyComponentType;
    Row: TableRowComponentType;
    Column: TableColumnComponentType;
};
type TableType = TableComponentType & TableCompositionType;


export const Table: TableType = TableComponent;
Table.Head = TableHeadComponent;
Table.Body = TableBodyComponent;
Table.Row = TableRowComponent;
Table.Column = TableColumnComponent;

ts 会抛出错误的地方。我的基本工作解决方案是:

export const Table: TableType = Object.assign(TableComponent, {
    Head: TableHeadComponent,
    Body: TableBodyComponent,
    Row: TableRowComponent,
    Column: TableColumnComponent,
});

唯一的缺点是,虽然结果将被类型检查,但对象参数内的各个子组件不会,这可能有助于调试。

一个好的做法是事先定义(和类型检查)参数。

const tableComposition: TableCompositionType = {
    Head: TableHeadComponent,
    Body: TableBodyComponent,
    Row: TableRowComponent,
    Column: TableColumnComponent,
};

export const Table: TableType = Object.assign(TableComponent, tableComposition);

但由于Object.assign是通用的,这也是有效的:

export const Table = Object.assign<TableComponentType, TableCompositionType>(TableComponent, {
    Head: TableHeadComponent,
    Body: TableBodyComponent,
    Row: TableRowComponent,
    Column: TableColumnComponent,
});

当然,如果您不需要(或想要)事先明确指定类型,您也可以这样做,它只会被推断出来。不需要讨厌的黑客。

export const Table = Object.assign(TableComponent, {
    Head: TableHeadComponent,
    Body: TableBodyComponent,
    Row: TableRowComponent,
    Column: TableColumnComponent,
});

使用纯 React 功能组件,我是这样做的:

如何使用

import React, {FC} from 'react';
import {Charts, Inputs} from 'components';

const App: FC = () => {

    return (
        <>
            <Inputs.Text/>
            <Inputs.Slider/>

            <Charts.Line/>
        </>
    )
};

export default App;

组件层次结构

 |-- src
    |-- components
        |-- Charts
            |-- components
                |-- Bar
                    |-- Bar.tsx
                    |-- index.tsx
                |-- Line
                    |-- Line.tsx
                    |-- index.tsx
        |-- Inputs
            |-- components
                |-- Text
                    |-- Text.tsx
                    |-- index.tsx
                |-- Slider
                    |-- Slider.tsx
                    |-- index.tsx

代码

您的最终组件,如Text.tsx,应如下所示:

import React, {FC} from 'react';


interface TextProps {
    label: 'string'
}

const Text: FC<TextProps> = ({label}: TextProps) => {

    return (
        <input
            
        />
    )
};

export default Text;

并且index.tsx喜欢:

src/components/index.tsx

export {default as Charts} from './Charts';
export {default as Inputs} from './Inputs';

src/components/Inputs/index.tsx

import {Text, Slider} from './components'

const Inputs = {
    Text, 
    Slider
};

export default Inputs;

src/components/Inputs/components/index.tsx

export {default as Text} from './Text';
export {default as Slider} from './Slider';

src/components/Inputs/components/Text/index.tsx

export {default} from './Text';

这就是仅使用 ES6 导入/导出即可实现点符号的方法