typescript-cheatsheets/react

How to handle conditional rendering?

smashercosmo opened this issue ยท 19 comments

Consider the following example:

type Component1Props = { ... }
const Component1: React.SFC<Component1Props> = props => { ... }

type Component2Props = { ... }
const Component2: React.SFC<Component2Props> = props => { ... }

const Component3: React.SFC<???> = props => {
   // some prop from Component1Props
   if(props.prop1) {
      return <Component1 {...props} />
   }
   
   // some prop from Component2Props
   if(props.prop2) {
      return <Component2 {...props} />
   }
}

Ideally I want typechecker to handle two things:

  1. It's required to pass either prop1 or prop2, but not both of them
  2. If prop1 is passed then props are of type Component1Props and if prop2 is passed then props are of type Component2Props

P. S. I'm completely new to TypeScript and I'm not sure that it's even possible)

I will try to check more but for a quick feedback try:

type IOneOfThem = Component1Props | Component2Props;

Yeah, that was the first thing that I tried. Didn't work)

Will try to check this in the next two hours

this works for me... does that fit your use case @smashercosmo

type Component1Props = { a1: boolean };
const Component1: React.SFC<Component1Props> = props => <div>{props.a1}</div>;

type Component2Props = { a2: boolean };
const Component2: React.SFC<Component2Props> = props => <div>{props.a2}</div>;

const Component3 = (props: {showprop1: boolean} & (Component1Props | Component2Props)) => {
   // some prop from Component1Props
   const {showprop1, ...rest} = props;
   if (showprop1) {
      return <Component1 {...rest as Component1Props} />;
   } else {
      return <Component2 {...rest as Component2Props} />;
   }
};

Well, not exactly) Consider real-life example

import React from 'react'
import { NavLink, NavLinkProps } from 'react-router-dom'

type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>

const Link = (props: ???) => {
   if (props.to) {
      return <NavLink {...props as NavLinkProps} />;
   } else {
      return <a {...props as AnchorProps} />;
   }
};

export default Link;

If user passes 'to' property NavLink should be rendered, if user passes 'href' then simple anchor tag should be rendered, but passing both 'to' and 'href' should result in compilation error). So at least one of two properties (to and href) is required, but passing both of them is forbidden.

P.S. Why aren't you using React.SFC in 3rd component?

cos i dont like React.SFC haha

hmm, so you want compile time error if both to and href are passed. this is stumping me.

I think that is related more to TypeScript than being related to TypeScript-React.

I was busy today.. but I'll invest some time in the next hours

This seems to work but it feels too much boilerplate for little gain, maybe there is a better way to do it. (Note that NavLinkProps contains 'href' so you need to omit it to work as you expect)

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

interface LinkProps {}

type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>
type RouterLinkProps = Omit<NavLinkProps, 'href'>

const Link = <T extends {}>(
    props: LinkProps & T extends RouterLinkProps ? RouterLinkProps : AnchorProps
) => {
    if ((props as RouterLinkProps).to) {
        return <NavLink {...props as RouterLinkProps} />
    } else {
        return <a {...props as AnchorProps} />
    }
}

<Link<RouterLinkProps> to="/">My link</Link> // ok
<Link<AnchorProps> href="/">My link</Link> // ok
<Link<RouterLinkProps> to="/" href="/">My link</Link> // error

wow. i think writing a React component with a generic type like that should definitely be included in this list, i really really like it. i wonder if we can boil this example down a bit simpler though:

// this is just a draft, i dont know what i'm really going for here yet
const Link = <T>(props: LinkProps & T) => {
	return <NavLink {...props} />
}

i realize this doesnt answer OP's original question, im just trying to figure out what i can add to the cheatsheet for this general category of problem. something like a polymorphic react component.

I used the generic + props merging when I used abstract React Class Components.. it's so powerful

Does this help?

interface IComponent1Props {
  foo: string
}

interface IComponent2Props {
  bar: string
}

const Component1: React.SFC<IComponent1Props> = ({ foo }) => (
  <div>component 1: {foo}</div>
)
const Component2: React.SFC<IComponent2Props> = ({ bar }) => (
  <div>component 2: {bar}</div>
)

type Foo = { prop1: true } & IComponent1Props
type Bar = { prop2: true } & IComponent2Props

const isFoo = (test: Foo | Bar): test is Foo => {
  return (test as Foo).prop1 === true
}

const isBar = (test: Foo | Bar): test is Bar => {
  return (test as Bar).prop2 === true
}

const Component3: React.SFC<Foo | Bar> = (props) => {
  if (isFoo(props)) {
    const { prop1, ...rest } = props
    return <Component1 {...rest} />
  } else if (isBar(props)) {
    const { prop2, ...rest } = props
    return <Component2 {...rest} />
  }

  return null
}

this is verbose but very readable. didnt know you could do tests like that!

i am going to add a link to this discussion. this has been very helpful to me.

You're welcome. I was lazy so i didn't name the types well ๐Ÿ˜†

@codepunkt you solution can be implemented with much less code using Discriminated Unions, but still it's annoying to specify extra props to differentiate one component from another.

@jpavon solution works, but again, it's annoying to specify generic type every time

I wonder why type guards don't work

import React from 'react'
import {
  Link as ReactRouterLink,
  LinkProps as ReactRouterLinkProps,
} from 'react-router-dom'

type LinkProps =
  | ReactRouterLinkProps
  | React.AnchorHTMLAttributes<HTMLAnchorElement>

function isAnchorProps(
  props: LinkProps,
): props is React.AnchorHTMLAttributes<HTMLAnchorElement> {
  return (
    (props as React.AnchorHTMLAttributes<HTMLAnchorElement>).href !== undefined
  )
}

export function Link(props: LinkProps) {
  if (isAnchorProps(props)) {
    return <a {...props} />
  }

  return <ReactRouterLink {...props} />
}

If I then use Link component like this

<Link href="/hi" to="/hi">
    Hello
</Link>

TypeScript doesn't raise any error

There is this amazing blog by @andrewbranch on Discriminated Unions:
https://blog.andrewbran.ch/expressive-react-component-apis-with-discriminated-unions/

Can we add this somewhere in the cheat-sheet readme. (Amazing blog, hover over variables in the snippets and see the definition)

There are a few caveats, mentioned here and also in issues microsoft/TypeScript#29703 and microsoft/TypeScript#28131

I was very late to this party, but the recommended way of doing conditional rendering is a generic function for function components (or generic constructors for class components), unfortunately that does mean you can't use the SFC annotation. Everyone's already covered what I was going to add ๐Ÿ™ƒ

@azizhk yup i already put it in https://github.com/typescript-cheatsheets/react-typescript-cheatsheet/blob/master/ADVANCED.md#typing-a-component-that-accepts-different-props but its a bit buried. i havent given much thought to how this section can be organized but i hope that people read every bit thoroughly and folow through on the links.

gonna consider this closed for now, open a new issue if people wanna re-ask stuff :)

(Thanks for the kind words, @azizhk ๐Ÿ˜„)