Reputation: 14873
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
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
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
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
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
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>
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
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