25

Official ReactJs documentation recommends to create components following the dot notation like the React-bootstrap library:

<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>

Thanks to this question, I know that I can create this structure using functional components just like that in javascript:

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

Card.Body = Body

export default Card

Using TypeScript I decided to add the corresponding types to it:

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

Problem is now that TypeScript don't allow the assignment Card.Body = Body and give me the error:

Property 'Body' does not exist on type 'FunctionComponent<{}>'

So how can I type this correctly in order to use this code structure?

johannchopin
  • 13,720
  • 10
  • 55
  • 101

5 Answers5

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

Card.Body = Body;

Or more readable:

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;
Roberto Zvjerković
  • 9,657
  • 4
  • 26
  • 47
7

I found a neat way using Object.assign to make dot notation work with ts. There were use cases similar to

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;

where ts would throw errors. My basic working solution was:

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

The only drawback is that while the result will be typechecked, the individial subcomponents inside the object parameter wouldn't be, which might be helpful for debugging.

A good practice would be to define (and typecheck) the parameter beforehand.

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

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

But since Object.assign is generic, this is also valid:

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

Of course, if you don't need to (or want to) explicitly specify the type beforehand, you can also do that and it will just get inferred. No nasty hacks required.

export const Table = Object.assign(TableComponent, {
    Head: TableHeadComponent,
    Body: TableBodyComponent,
    Row: TableRowComponent,
    Column: TableColumnComponent,
});
Filip Kaštovský
  • 1,866
  • 8
  • 12
5

After spending a lot of time figuring out how to use dot notation with forwardRef components, this is my implementation:

Card Body Component:

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";

Card Component:

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;

And using it in your code would look something like this:

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

Bonus Feature

If you want a specific order:

...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

!!! If children is not present you will get an error when trying to add anything else besides the CardBody component. This use-case is very specific, though could be useful sometimes.

You can ofcourse continue adding components (Header, Footer, Image, etc.)

dodobrat
  • 321
  • 4
  • 4
0

In this case it is very handy to use typeof to save some refactoring time if the sub component type changes:

type CardType = React.FunctionComponent<CardPropsType> & { Body: typeof Body };

const Card: CardType = (props: CardPropsType) => {
  return <>{props.children}<>;
}

Card.Body = Body;

https://www.typescriptlang.org/docs/handbook/2/typeof-types.html

Andreas Riedmüller
  • 1,217
  • 13
  • 17
-1

With pure React functional components, I do it like so :

How to use

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

const App: FC = () => {

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

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

export default App;

Components hierarchy

 |-- 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

Code

Your final component, like Text.tsx, should look like so :

import React, {FC} from 'react';


interface TextProps {
    label: 'string'
}

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

    return (
        <input
            
        />
    )
};

export default Text;

and index.tsx like :

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';

that's how you can achieve dot notation using only ES6 import / export

paul.ruelle
  • 132
  • 2
  • 11