user101289
user101289

Reputation: 10422

React and Typescript dynamic component from config

I'm trying to build a dynamic menu system with reactjs and typescript.

My config looks like this:

import {TableIcon} from "@heroicons/react/solid";

type route = {
    icon: React.ReactNode,
    path: string,
    title: string,
}

export const navRoutes = () : route[] => {
    return [
        {
            icon: TableIcon,
            path: '/',
            title: 'Home'
        },
    ]
}

in my nav component, I'm doing

{navRoutes().map((item) => (
    <a key={item.title} href={item.path}>
        <item.icon /> // also tried {item.icon}
        {item.title}
    </a>
))}

I'm getting an error that TS2604: JSX element type 'item.icon' does not have any construct or call signatures.

I've done similar things without typescript that worked as expected-- can anyone tell me what I am doing wrong?

Upvotes: 0

Views: 1854

Answers (3)

Andrew Ross
Andrew Ross

Reputation: 1158

You can use JSX.Element instead of React.ReactNode, that's what @heroicons/react returns as a type

Footer Social Icons (added the TableIcon for this example)

import { FC } from 'react';
import cn from 'classnames';
import {
    Facebook,
    Instagram,
    SquareLogo
} from '@/components/UI/Icons';
import { TableIcon } from '@heroicons/react/solid';
import css from './footer.module.css';

export interface FooterSocialProps {
    href: string;
    label: string;
    id: number;
    icon: JSX.Element;
}

export const footerSocial: FooterSocialProps[] = [
    {
        href: 'https://www.facebook.com/thefaderoominc/?ref=py_c',
        label: 'Facebook',
        id: 0,
        icon: <Facebook />
    },
    {
        href: 'https://www.instagram.com/thefaderoomhighlandpark/',
        label: 'Instagram',
        id: 1,
        icon: <Instagram />
    },
    {
        href: 'https://squareup.com/gift/MLHZCDVC0MKB1/order',
        label: 'Square Giftcards',
        id: 2,
        icon: <SquareLogo />
    },
    {
        href: 'https://stackoverflow.com/example',
        label: 'Table Icon',
        id: 3,
        icon: <TableIcon />
    }
];

export interface FooterSocialPropsFC {
    className?: string;
}

const FooterSocial: FC<FooterSocialPropsFC> = ({
    className
}) => {
    return (
        <div className={cn(css.socialRoot, className)}>
            {footerSocial.map((social, i) => (
                <div key={++i}>
                    <a
                        title={social.label}
                        target='__blank'
                        href={social.href}
                        className={cn(
                            css.socialLink,
                            'text-olive-300 hover:text-olive-400  text-opacity-80'
                        )}
                    >
                        <span className='sr-only'>
                            {`External Link to The Fade Room's ${social.label} page`}
                        </span>
                        {social.icon}
                    </a>
                </div>
            ))}
        </div>
    );
};

export default FooterSocial;

Upvotes: 1

Linda Paiste
Linda Paiste

Reputation: 42288

The type React.ReactNode describes the returned value from calling a JSX component rather than the component itself. You can either:

1 ) Pass a React.ReactNode:

type route = {
  icon: React.ReactNode;
  path: string;
  title: string;
};

export const navRoutes = (): route[] => {
  return [
    {
      icon: <TableIcon />,
      path: "/",
      title: "Home"
    }
  ];
};

{navRoutes().map((item) => (
  <a key={item.title} href={item.path}>
    {item.icon}
    {item.title}
  </a>
))}
  1. Pass a callable component. Usually you need to give it an uppercase name in order to call it through JSX. Somehow <item.icon/> seems like it works? But to be safe I would capitalize it.
type route = {
  icon: React.ComponentType;
  path: string;
  title: string;
};

export const navRoutes = (): route[] => {
  return [
    {
      icon: TableIcon,
      path: "/",
      title: "Home"
    }
  ];
};

{navRoutes().map(({title, path, icon: Icon}) => (
  <a key={title} href={path}>
    <Icon/>
    {title}
  </a>
))}

By default React.ComponentType doesn't take any props (other than children). If you want to pass props when you call your <Icon/> you can use React.ComponentType<SomePropsType>.

Upvotes: 2

Alex Wayne
Alex Wayne

Reputation: 187262

React.ReactNode is the type of a rendered component. For example:

const a: React.ReactNode = <TableIcon />

If you want to pass in some rendered JSX, that's what you would use. But it sounds like you want to pass in a a functional component with no props, which will then be rendered.

React.FC is the generic type of a react functional component with no props. That's probably the type you want.

type route = {
    icon: React.FC,
    path: string,
    title: string,
}

Playground

Upvotes: 2

Related Questions