johannchopin
johannchopin

Reputation: 14873

Using dot notation with functional component in TypeScript

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?

Upvotes: 26

Views: 14240

Answers (6)

cambunctious
cambunctious

Reputation: 9651

Less is more when it comes to type annotations. Let TypeScript do its job and infer types for the component functions and the exported object. But do use Object.assign to tie the components together into a combined object.

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

export default Object.assign(Card, { Body });

Upvotes: 0

Andreas Riedm&#252;ller
Andreas Riedm&#252;ller

Reputation: 1489

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

Upvotes: 0

paul.ruelle
paul.ruelle

Reputation: 132

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

Upvotes: -1

Filip Kaštovsk&#253;
Filip Kaštovsk&#253;

Reputation: 2036

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,
});

Upvotes: 7

dodobrat
dodobrat

Reputation: 341

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

Upvotes: 5

Roberto Zvjerković
Roberto Zvjerković

Reputation: 10157

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;

Upvotes: 38

Related Questions