Search code examples
reactjstypescriptconditional-statementscomponents

How do I pass a variable as a react component while bypassing conditional render?


import { ReactComponent as GreenIcon } from '../assets/green.svg'
import { ReactComponent as YellowIcon } from '../assets/yellow.svg'
import { ReactComponent as RedIcon } from '../assets/red.svg'

const GreenIconMemoized = memo(GreenIcon)
const YellowIconMemoized = memo(YellowIcon)
const RedIconMemoized = memo(RedIcon)

const STATUSES = ['ready', 'delay', 'stop']
const icons = new Map([
  ['ready', GreenIconMemoized],
  ['delay', YellowIconMemoized],
  ['stop', RedIconMemoized]
])

const [sampleStatus, setSampleStatu] = useObserver(() => [
  sampleSTORE.sampleStatus,
  sampleSTORE.setSampleStatus
])

const sampleArray: ComponentType[] = useMemo(() => {
  return STATUSES.map((status: string) => {
    const Icon = icons.get(status)
    return {
      id: status,
      label: status,
      isSelected: status === sampleStatus,
      onSelect: (): void => {
        setSampleStatus(status)
      },
      leftComponent: Icon ? <Icon /> : undefined
    }
  })
}, [sampleStatus])

In the code above, what is the best way to pass the Icon value as a react component without the need of a conditional render? Both STATUSES and icons are hard coded into the system, and therefore are not really variable - the else condition will never be reached. But if I attempt to pass for example:

...
   leftComponent: <Icon />
...

I am given the error "JSX element type 'Icon' does not have any construct or call signatures."


Solution

  • I would recommend using an object instead of a Map. That way, typescript can know exactly which keys are valid, and thus knows that undefined is impossible (as long as you provide a valid key).

    const STATUSES = ['ready', 'delay', 'stop'] as const;
    const icons = {
      ready: GreenIconMemoized,
      delay: YellowIconMemoized,
      stop: RedIconMemoized
    }
    
    //...
    return STATUSES.map((status) => {
      const Icon = icons[status];
      // ...
        leftComponent: <Icon />
      // ...
    

    Note that i used as const on the STATUSES. This causes typescript to infer the type to be readonly ["ready", "delay", "stop"], not string[]. That means that later, when you map over the array, status is of type "ready" | "delay" | "stop", not string, and therefore you've proven to typescript that you'll only access valid keys.

    If you prefer, you could do the types more explicitly, as in:

    const STATUSES: ("ready" | "delay" | "stop")[] = ['ready', 'delay', 'stop'];
    // ...
    return STATUSES.map((status: "ready" | "delay" | "stop") => {
    
    // OR:
    const STATUSES: (keyof typeof icons)[] = ['ready', 'delay', 'stop'];
    // ...
    return STATUSES.map((status: keyof typeof icons) => {