Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

False positives of react/prop-types in forwardRefs with 7.27.0 #3140

Closed
gunters63 opened this issue Nov 16, 2021 · 24 comments · Fixed by #3280
Closed

False positives of react/prop-types in forwardRefs with 7.27.0 #3140

gunters63 opened this issue Nov 16, 2021 · 24 comments · Fixed by #3280

Comments

@gunters63
Copy link

prop type checking seems to broken inside forwardRef with version 7.27.0, 7.26.1 still worked fine.

The error only happens for type intersection hierarchies (I didn't test interface extensions).

When you have types like this:

type ControlProps = {
  className?: string | undefined;
};

type NamedProps = {
  name: string;
};

type ControlPropsWithChildren = ControlProps & {
  children?: ReactNode;
};

type BaseButtonProps = ControlPropsWithChildren &
  NamedProps & {
    onClick?: (() => void) | undefined;
    onMouseDown?: (() => void) | undefined;
    onMouseUp?: (() => void) | undefined;
    disabled?: boolean | undefined;
    width?: number;
    type?: 'submit' | 'reset' | 'button' | undefined;
  };

The plugin will now falsely flag all properties in nested type intersections as missing in props validation, in this case name, className and children (see screenshot wiggly lines).

The problem does not happen when the component is not wrapped in forwardRef.
It also didn't happen with version 7.26.1

D:\...\BaseButton.tsx
  23:7  error  'name' is missing in props validation       react/prop-types
  24:7  error  'className' is missing in props validation  react/prop-types
  28:7  error  'children' is missing in props validation   react/prop-types

image

Its likely that this commit: 4bc3499 triggered the problem

@ljharb
Copy link
Member

ljharb commented Nov 16, 2021

cc @vedadeepta

@vaniyokk

This comment has been minimized.

@mataspetrikas
Copy link

mataspetrikas commented Nov 22, 2021

I have noticed a potential cause/fix.

The validation fails in the following case:

type InputTextProps = InputHTMLAttributes<HTMLInputElement> & {
  label: string;
  helperText?: string;
  error?: boolean;
};

export default forwardRef<HTMLInputElement, InputTextProps>(function InputText(
  props: InputTextProps,
  forwardedRef: Ref<HTMLInputElement>,
) {
  const { name, value, label, error = false, helperText, ...rest } = props;
  const [isFocused, setIsFocused] = useState<boolean>(false);
  const isActive = (typeof value === 'string' && value.length > 0) || isFocused;

  function handleFocus(event: FocusEvent<HTMLInputElement>) {
    setIsFocused(true);
    props.onFocus?.(event);
  }

  function handleBlur(event: FocusEvent<HTMLInputElement>) {
    if (!event.target.value) setIsFocused(false);
    props.onBlur?.(event);
  }

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    props.onChange?.(event);
  }

  return (
    <div className="relative">
      <input
        autoComplete="off"
        {...rest}
        value={value || ''}
        name={name}
        onChange={handleChange}
        onFocus={handleFocus}
        onBlur={handleBlur}
        ref={forwardedRef}
      />
      <label
        htmlFor={name}
      >
        {label}
      </label>
      {helperText && (
        <div>
          {helperText}
        </div>
      )}
    </div>
  );
});

but it's all good when we change the name of the parameter to something else than props:


export default forwardRef<HTMLInputElement, InputTextProps>(function InputText(
  componentProps: InputTextProps,
  forwardedRef: Ref<HTMLInputElement>,
) {
  const { name, value, label, error = false, helperText, ...rest } = componentProps;

Do we have a false positive that matches only the component props?

@gunters63
Copy link
Author

Interesting.

On the other hand, the validation still fails when you destructure right away in the function parameters:

const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
  (
    {
      name,
      className,
      onClick,
      onMouseDown,
      onMouseUp,
      children,
      disabled,
      width,
      type,
    }: BaseButtonProps,
    ref,
  ): JSX.Element => (

@nzdjb
Copy link

nzdjb commented Nov 23, 2021

I've put together a reasonably minimal example that triggers this:

import React, { forwardRef } from 'react'

interface D {
  x: string
  y: string
  z: string
}
interface C extends 
  Omit<D, 'z'> {}
const A = forwardRef<HTMLDivElement, C>(({ x, y }, ref) => {
  return (
    <div ref={ref}>
      {x} {y}
    </div>
  )
})

Output:

/workspaces/redwood-type-lint-issue/index.tsx
  9:44  error  'x' is missing in props validation  react/prop-types
  9:47  error  'y' is missing in props validation  react/prop-types

✖ 2 problems (2 errors, 0 warnings)

Remove the omission and the props are validated correctly:

interface C extends 
  D {}

Issue seems to be that type is missing at https://github.com/yannickcr/eslint-plugin-react/blob/94826da35804b7ea1ae5adcd6ba4b8e049418cf1/lib/rules/prop-types.js#L107

@vedadeepta
Copy link
Contributor

vedadeepta commented Nov 28, 2021

@gunters63 i'm not able to replicate.

I pulled the latest and put your code almost exactly in the test case and it passed, maybe some intermediate commit fixed it

Here is the sample test case

  valid: parsers.all([].concat(
    {

      code: `

        import React, { forwardRef } from 'react';
        type ControlProps = {
          className?: string | undefined;
        };

        type NamedProps = {
          name: string;
          test: number;
        };

        type ControlPropsWithChildren = ControlProps & {
          children?: ReactNode;
        };

        type ButtonProps = ControlPropsWithChildren &
          NamedProps & {
            onClick?: (() => void) | undefined;
            onMouseDown?: (() => void) | undefined;
            onMouseUp?: (() => void) | undefined;
            disabled?: boolean | undefined;
            width?: number;
            type?: 'submit' | 'reset' | 'button' | undefined;
        };

        const BaseButton = forwardRef<HTMLButtonElement, ButtonProps>((
          {
            name,
            className,
            onClick,
            onMouseDown,
            onMouseUp,
            children,
            disabled,
            width,
            type,
          },
          ref,
        ): JSX.Element => {
          return <span>{width}</span>
        })

      `,
      features: ['ts', 'no-babel'],

    },
  )),

  invalid: []

cc @ljharb

@nzdjb yes my PR didn't handle special TS types like Omit, Partial, Exclude etc. That will require a separate PR and I'm not sure about the effort. Not planning to do it in the next few weeks. Apologies.

@ljharb
Copy link
Member

ljharb commented Nov 28, 2021

Ideally we wouldnt have to explicitly support such things; the TS parser should have the type information we need already in the AST, thereby handling every possible type.

@gunters63
Copy link
Author

This starts to get mysterious :)

If I inline the type definitions like this:

type NamedProps = {
  name: string;
};

type ControlProps = {
  className?: string | undefined;
};

type ControlPropsWithChildren = ControlProps & {
  children?: ReactNode;
};

type BaseButtonProps = ControlPropsWithChildren &
  NamedProps & {
    onClick?: (() => void) | undefined;
    onMouseDown?: (() => void) | undefined;
    onMouseUp?: (() => void) | undefined;
    disabled?: boolean | undefined;
    width?: number;
    type?: 'submit' | 'reset' | 'button' | undefined;
  };

const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
  (
    {
   ....

the error goes away.

But my code originally does this:

import { ControlPropsWithChildren, NamedProps } from '$/components/BaseControl';

type BaseButtonProps = ControlPropsWithChildren &
  NamedProps & {
    onClick?: (() => void) | undefined;
    onMouseDown?: (() => void) | undefined;
    onMouseUp?: (() => void) | undefined;
    disabled?: boolean | undefined;
    width?: number;
    type?: 'submit' | 'reset' | 'button' | undefined;
  };

const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
  (
    {
   ....

Contents of $/components/BaseControl:

import { ReactNode } from 'react';

export type ControlProps = {
  className?: string | undefined;
};

export type NamedProps = {
  name: string;
};

export type ControlPropsWithChildren = ControlProps & {
  children?: ReactNode;
};

If I import the base types like the code initially does the error comes back.

@vedadeepta
Copy link
Contributor

@gunters63 ahh i see, if i understand correctly, currently we cannot parse types coming from external files.

@ljharb any thoughts on how to handle types coming from other files?

@gunters63
Copy link
Author

@vedadeepta:

I have many other components using those external types which do not use forwardRef. Those work fine.

@ljharb
Copy link
Member

ljharb commented Nov 29, 2021

@vedadeepta the TS parser should provide all those types; we shouldn't have to figure them out ourselves.

@JameelKhan9
Copy link

JameelKhan9 commented Dec 8, 2021

Can also confirm exporting the type in the same file breaks only for forward refs

"@types/react": "^17.0.37",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",

image

The following is with the export:
image

In saying this, I have no idea why exporting the type would break eslint from getting the proptype. 😕

seems similar to the discussion happening at #3015

sh0ji added a commit to wwnorton/design-system that referenced this issue Dec 18, 2021
recent release of eslint-plugin-react introduced
a bug related to type checking props inside functions:
- jsx-eslint/eslint-plugin-react#3140
- jsx-eslint/eslint-plugin-react#2760
@vedadeepta
Copy link
Contributor

vedadeepta commented Dec 21, 2021

@ljharb we're not getting the imported types in the AST. I checked the AST by creating a sample repo and importing types from other files.

Also its not just that the imported types don't work with forwardRef<> but they also don't work in general. we just don't throw an error for other cases.

I don't think this case can be handled without using the TS compiler API which can get the types from other files.

@gunters63
Copy link
Author

@vedadeepta: As long as this is not implemented, wouldn't it be better to remove the special handling of forwardRef introduced in commit 4bc3499?

@vedadeepta
Copy link
Contributor

vedadeepta commented Dec 21, 2021

Maybe, I'm not sure. This does not work for other React generic like FC<> etc, the only difference is that it fails silently

import { ControlProps, NamedProps } from './props';

type ButtonProps = ControlProps &
      NamedProps & {
        text: string;
    };
const Person2: React.FC<ButtonProps> = (props) => (
  <span>{props.somethingNotDef}</span> // <-- react/prop-types Does not throw an error but it should,
);

So we will need to remove all ts handling since it cannot get types imported from other files

@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

In that case yes, we should remove it.

@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

Or at least, it should only warn when the entire type is defined in the same file?

@JDMathew
Copy link

JDMathew commented Jan 10, 2022

An intermediate solution would be to wrap the FC export like so:

export interface IInputFieldProps extends InputHTMLAttributes<HTMLInputElement> {
...
}

const InputField = (props: IInputFieldProps, forwardedRef: Ref<HTMLInputElement>,) => {
return (
      <>
        <input {...props} ref={forwardedRef}></input>
      </>
    )
  });

export default forwardRef<HTMLInputElement, IInputFieldProps>(InputField);

@aakash-lohono
Copy link

hi,
another intermediate fix is to specify the argument types explicitly, like so

import React, { ForwardedRef, forwardRef } from "react";

export interface HelloWorldProps {
  subject: string;
  className?: string;
}

const HelloWorld = forwardRef(
  function HelloWorldBase (
    props: HelloWorldProps,
    ref: ForwardRef<HtmlDivElement>,
  ) {
    const { subject, className } = props;

    return (
      <div 
        className={className}
        ref={ref}
      >
        Hello {subject}
      </div>
    );
  },
);

export default HelloWorld;

@gunters63
Copy link
Author

hi, another intermediate fix is to specify the argument types explicitly, like so

Nice find! This workaround is low-effort and works well, I finally can unpin the package version again :)

@ljharb
Copy link
Member

ljharb commented Feb 22, 2022

@vedadeepta any chance you'd be able to work on a PR for this?

@mobily
Copy link
Contributor

mobily commented Mar 12, 2022

@ljharb I can take it if you and @vedadeepta don't mind :)

@ljharb
Copy link
Member

ljharb commented Mar 12, 2022

That'd be great!

@vedadeepta
Copy link
Contributor

thanks @mobily

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

10 participants