typescript-cheatsheets / react

Cheatsheets for experienced React developers getting started with TypeScript

Home Page:https://react-typescript-cheatsheet.netlify.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Question] Conditional Rendering with forwardRef

mrtnbroder opened this issue · comments

Hi there!

Awesome cheat-sheet, it helped me a lot! Thanks for that.

However, I'm facing an issue atm where a simple type overload isn't working for me, or I just dont know how to type it when adding a simple forwardRef infront of the conditional component.

to the playground!

Is there any way to add a type overload for the forwardRef function here?

EDIT:

I got it working with a workaround! (finally omg)

link

I would still loooooove to get it working with React.forwardRef though!

hey! sorry for slow response, been busy for a bit. I'll have a look but frankly havent needed to use forwardRef much 😅 i think you got forwardRef working

No worries. I got it working - yes, but I have to use a workaround with forwardRef prop. I was wondering if this can be achieved with React.forwardRef<A, B> as well.

Because of how higher order functions work with functions that have overload signatures, there's not really any easy way of doing this. My best recommendation for the happy middle is to have a union of your different variants as your props for the render function, and to type the output manually:

import * as React from 'react'

interface ButtonProps extends Omit<JSX.IntrinsicElements['button'], 'ref'> {
  href?: undefined
}
interface AnchorProps extends Omit<JSX.IntrinsicElements['a'], 'ref'> {
  href: string // this should actually be required for TS to properly discriminate the two
}

type PolymorphicProps = ButtonProps | AnchorProps
type PolymorphicButton = {
  (props: AnchorProps): JSX.Element
  (props: ButtonProps): JSX.Element
}

const Button: PolymorphicButton = React.forwardRef(
  (props: PolymorphicProps, ref: any) => {
        return props.href != null
            ? <a {...props} ref={ref} />
            : <button {...props} ref={ref} />
  }
) as any

// e is inferred as MouseEvent<HTMLAnchorElement>
const AnchorButton = <Button href='abc' onClick={e => {}} />
// e is inferred as MouseEvent<HTMLButtonElement>
const ButtonButton = <Button onClick={e => {}} />

Playground link.

This example actually gives an error with TS 4.1.3 when you pass ref to the component:

Link

havent had a chance to look but fix welcome

So I've tried to hack some things, seems like it works, but maybe there is a better way to type it?

Link

Code just in case:

import * as React from 'react';

type ButtonProps = JSX.IntrinsicElements['button'] & {
  href?: undefined;
}

type AnchorProps = JSX.IntrinsicElements['a'] & {
  href: string;
}

// Does not work for some reason
interface ButtonProps2 extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  // Does not work, can't pass ref
  // interface ButtonProps2 extends Omit<JSX.IntrinsicElements['button'], 'ref'> {
  href?: undefined;
}

// Does not work for some reason
interface AnchorProps2 extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  // Does not work, can't pass ref
  // interface AnchorProps2 extends Omit<JSX.IntrinsicElements['a'], 'ref'> {
  href: string;
}

type PolymorphicProps = ButtonProps | AnchorProps;
type PolymorphicButton = {
  (props: AnchorProps): JSX.Element;
  (props: ButtonProps): JSX.Element;
};

const isAnchor = (props: PolymorphicProps): props is AnchorProps => {
  return props.href != undefined;
};

export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, PolymorphicProps>(
  (props, ref) => {
    return isAnchor(props) ? (
      <a
        {...props}
        ref={ref as React.ForwardedRef<HTMLAnchorElement>}
      />
    ) : (
        <button
          {...props}
          ref={ref as React.ForwardedRef<HTMLButtonElement>}
        />
      );
  },
) as PolymorphicButton;

const refButton = React.createRef<HTMLButtonElement>();
const refAnchor = React.createRef<HTMLAnchorElement>();

// Need to use null as default, error otherwise
const useRefButton = React.useRef<HTMLButtonElement>(null)
const useRefAnchor = React.useRef<HTMLAnchorElement>(null)

const tests = <>
  // All fine with createRef, event inferred
  <Button ref={refButton} className="class" onClick={e => console.log(e)} />
  <Button ref={refAnchor} className="class" href="someHref" onClick={e => console.log(e)} />

  // All fine with useRef, event inferred
  <Button ref={useRefButton} className="class" onClick={e => console.log(e)} />
  <Button ref={useRefAnchor} className="class" href="someHref" onClick={e => console.log(e)} />

  // Button can be disabled
  <Button ref={useRefButton} disabled className="class" onClick={e => console.log(e)} />
  // Anchor cannot
  <Button ref={useRefAnchor} disabled className="class" href="someHref" onClick={e => console.log(e)} />

  // Not valid type for button
  <Button ref={refButton} type="wow" onClick={e => console.log(e)} />
  // Ok now with valid type="submit"
  <Button ref={refButton} type="submit" onClick={e => console.log(e)} />
  // Anchor can have some type too, it's valid
  <Button ref={refAnchor} href="someHref" type="submit" onClick={e => console.log(e)} />
  <Button ref={refAnchor} href="someHref" type="nonButtonType" onClick={e => console.log(e)} />

  // Anchor ref is not valid for button
  <Button ref={refAnchor} onClick={e => console.log(e)} />
  <Button ref={useRefAnchor} onClick={e => console.log(e)} />
  // Button ref is not valid for anchor
  <Button ref={refButton} href="someHref" onClick={e => console.log(e)} />
  <Button ref={useRefButton} href="someHref" onClick={e => console.log(e)} />
</>

great submission! im not spending any time to validate this right now, but i'll update the docs if anyone else does. thank you!

commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions!