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

button-has-type does not allow for dynamic assignment #1555

Closed
pke opened this issue Nov 20, 2017 · 53 comments
Closed

button-has-type does not allow for dynamic assignment #1555

pke opened this issue Nov 20, 2017 · 53 comments

Comments

@pke
Copy link

pke commented Nov 20, 2017

My Button component allows to override the type arg but button-has-type is still complaining about. It seems it does not validate the real AST path of prop assignment

Button = ({ type = "button" }) => <button type={type}/>
Button.propTypes = {
  type: PropTypes.arrayOf(["submit", "button", "reset"]),

Does the plugin have access to the propTypes and can match that the only possible allowed values are within the rules allowed values and that the default type is also set to something specific, like in my example to button?

@ljharb
Copy link
Member

ljharb commented Nov 20, 2017

Stateless React Components should not use JS default values; that needs to be a defaultProp.

@pke
Copy link
Author

pke commented Nov 20, 2017

When you say "should not" where does this recommendation come from? Is this a lint suggestion or are there language/runtime implications?

@ljharb
Copy link
Member

ljharb commented Nov 20, 2017

defaultProps are introspectable by React and other tools; default arguments are not. #1184 has more discussion on it.

@lydell
Copy link

lydell commented Feb 1, 2018

Regardless of the defaultProps discussion, something’s buggy with this rule.

The following code:

<button type={submit ? "submit" : "button"} onClick={onClick}>
  {children}
</button>

… gives the following lint error:

"null" is an invalid value for button type attribute

Surely it shouldn’t say "null" there?

@ljharb
Copy link
Member

ljharb commented Feb 2, 2018

@lydell very true! the ternary case would probably have to be explicitly handled; however, I'd say that the rule should be forcing a single, hardcoded type value - and that you should conditionally render two different elements if you want two types.

@mikebarnhardt
Copy link

I agree this rule is handled poorly. defaultProps with a type of either button, reset, or submit should not trigger.

A work-around is to just disable the rule temporarily:

/* eslint-disable react/button-has-type */
return <button {...passProps} className={buttonClassNames} type={type}/>;
/* eslint-enable react/button-has-type */

@ljharb
Copy link
Member

ljharb commented Mar 29, 2018

The type should not be dynamic, full stop.

If you want the type to be one of the 1, 2, or 3 options, use if/else and hardcode those three.

@pke
Copy link
Author

pke commented Apr 20, 2018

Why shouldn't it be dynamic? Can you explain please?

@brandonburkett
Copy link

I am also interested in "why" it should be dynamic (because dev typo in props?). Seems weird to duplicate the button three times for a hardcoded value.

htmlType: PropTypes.oneOf(['button', 'submit', 'reset']),
static defaultProps = { htmlType: 'button' }
<button type={htmlType || 'button'} />

@ljharb
Copy link
Member

ljharb commented Jul 2, 2018

Only twice, since reset buttons are a terrible UX pattern :-)

@ljharb
Copy link
Member

ljharb commented Jul 2, 2018

It shouldn’t be dynamic because all three button types have wildly different semantics - i can’t conceive of when you’d want a submit button randomly changing to be an entirely different kind of button, or vice versa.

A submit button must be inside a form; a regular button must have some kind of javascript behavior - these are different components/elements, not just two flavors of the same thing.

@brandonburkett
Copy link

Isn't that true for input types as a whole? Not just specific button? Why wouldn't this give an error as well?

type: PropTypes.oneOf(['text','email','tel','password','url','search','number','date','range','file']),
static defaultProps { type: 'text' }
<input type={type || 'text'} ... />

@ljharb
Copy link
Member

ljharb commented Jul 2, 2018

Yes, that is certainly true! However, the primary point of the rule is to ensure that button is used safely.

If we wanted to make a generic rule that governed input types, that would also force them to be static (and I'd encourage you not to make a non-private generic input component that takes "type" as a parameter, for the reasons I mentioned above)

@pke
Copy link
Author

pke commented Jul 2, 2018

In a REST level 3 kind of app where the app state is completely controlled by the server, the server dictates the fields of a form. That's a valid scenario for when the button type needs to be dynamic. Since hardcoding the now known button types defeats the entire reason for going REST level 3, which is to have an evolutionary API, which can change in the future without touching the client but just send a new HTML6 value for button type.

But since REST level 3 apps are such a rare breed as of now I'll just disable this rule that's supposed to make the button type usage "safe".

@ljharb
Copy link
Member

ljharb commented Jul 3, 2018

I'm not familiar with any HTML 6 nor any plans for it in the near or far future, nor any plans for any new button type ever. When one appears, the server shouldn't be controlling that - it should only be explicitly supported in the UI.

@pke
Copy link
Author

pke commented Jul 3, 2018

I think we can agree to disagree on that so I'll close this issue here. A workaround was mentioned that works just fine.
Thanks for all the work on this plugin!

@vaske
Copy link

vaske commented Nov 21, 2018

@pke which workaround works to you?

@pke
Copy link
Author

pke commented Nov 21, 2018

I don't remember @vaske :/ Maybe I just disabled the rule.

@vaske
Copy link

vaske commented Nov 21, 2018

@pke yeah I did the same ;)

@josephshambrook
Copy link

Yep, I disabled the rule too.

const Button = ({ type }) => {

  /*
   * JSX line disabled for ESLint due to questionable rule implementation
   */

  return (
    // eslint-disable-next-line react/button-has-type
    <button type={type} className="button">Test</button>
  );
};

Button.propTypes = {
  type: PropTypes.string,
}

Button.defaultProps = {
  type: 'button',
}

@ljharb
Copy link
Member

ljharb commented Nov 23, 2018

@josephshambrook at the least, make a propType that used oneOf, so you’re not just wildly accepting any random string (or worse, “reset”).

Using an override comment inside a component where you’re knowingly violating a best practice is indeed the proper way to handle it.

@pke
Copy link
Author

pke commented Nov 23, 2018

Since propTypes are not used in production builds anyway such safeguards against "reset" type should happen in code, not in propTypes.

@ljharb
Copy link
Member

ljharb commented Nov 23, 2018

If your propType warnings are properly set up to fail your tests, there’s no need to check it in production.

@mattdell
Copy link

mattdell commented Mar 18, 2019

Just to add, the above "solution" doesn't work for TypeScript.

interface Props {
  type: 'button' | 'submit' | 'reset';
}

const Button: React.StatelessComponent<Props> = ({
  children,
  type = 'button',
}) => (
  <button type={type}>{children}</button> 
);

I'm going to just assume I'll have to disable the rule.

@pke
Copy link
Author

pke commented Oct 23, 2019

but then you’ll be writing your runtime checks in addition to compile/test-time checks, if you’re that concerned with correctness.

Every decent software engineer should be that concerned with correctness. And no static type analysis or ASSERT will prevent invalid input. But I'm afraid this kind attitude is one the reasons there is so many sloppy insecure software released today.

On the other hand you did not answer my question if the team would accept a PR with the option to enabled dynamic type assigned for the many reasons voiced and voted here by users of the plugin? The option would default to the current restrictive behaviour but could be relaxed.

@ljharb
Copy link
Member

ljharb commented Oct 23, 2019

How would using this option be different than simply disabling the rule, or using an override comment? The rule, despite it's name, does not merely care if your button has a type attribute, the point is to assert it's a correct one - which your option does not ensure.

@robguy21
Copy link

The rule, despite it's name, does not merely care of your button has a type attribute,

Perhaps the name should change then to be more indicative of what it's actually doing.

A codebase I'm working on now has many different groups of buttons, and nearly all buttons are generated from an array of "object"s and then props are put onto the button as we iterate over the object.

There are definitely valid use cases to only want to check the existence of non-empty type property on a button.

If you don't want to support this that's alright but

The rule, despite it's name,

Clearly indicates a problem and should be looked at it. It's clearly misleading.

@thejamespower

This comment has been minimized.

@ljharb

This comment has been minimized.

@pke
Copy link
Author

pke commented Dec 4, 2019

You see, that's exactly the answer I was afraid would appear any time in this thread.

Many users get confused by the rule. Judging by all your arguments (against "reset" types) the rule should maybe have been named button-has-correct-type and correct means one and foremost "not reset". All the use cases users presented are basically dismissed by you by asking the users to change their code (into needlessly convoluted if/else structs) just to get rid of the false positive.

Responding to valid concerns regarding the usage of a rule should not be countered by "you can fork" or lecturing users on their code.

I am beyond this, as I do currently not work with react anymore but this "you can fork" just sounded rude to me considering all the above constructive feedback on this very opinionated rule.

But then maybe its also not easily possible to change the rule to support dynamic types ("reset" not included) even if the maintainers wanted to.

@ljharb
Copy link
Member

ljharb commented Dec 4, 2019

@pke my response saying to fork was to "eslint is supposed to be unopinionated", any package can be as opinionated as it wants to - this plugin is opinionated like every other.

I totally agree that changing the rule name would help, but the churn of doing that isn't worth it.

It's not at all practical and possible to support dynamic types; that's just the nature of JavaScript itself (it could be supported in extremely limited ways that would require massive complexity, but that wouldn't be sufficient for virtually any of the use cases folks have complained about)

@Hypnosphi
Copy link
Contributor

the ternary case would probably have to be explicitly handled

This sounds like a good solution to me. Would a PR adding this be welcome?

@ljharb
Copy link
Member

ljharb commented Aug 7, 2020

@Hypnosphi sure, seems harmless enough, as long as both branches in the ternary were static strings.

@musicMan1337
Copy link

I'm having the same Eslint issue, hence I found this page. Much of the conversation essentially claimed that buttons should be hard-coded due to the different 'schemas' needed for different button types. I came up with this:

export const Button = ({ className, type, ...props }) => {
  Button.defaultProps = { className: '', type: 'button' };

  /* eslint-disable react/button-has-type */
  return (
    <button
      className={['button', className].join(' ')}
      type={type}
      {...props}
    />
  );
};

As you can see, I was forced to disable the linter here, but as far as dynamic, this is about as good as I could make it to support every style of button via overloading, including if you wanted to add additional class names.

I've only been coding for about 2 months, so any feedback (positive AND constructive) would be greatly appreciated!

@Hypnosphi
Copy link
Contributor

Hypnosphi commented Oct 8, 2020

@musicMan1337 do you ever use <Button type="submit" />? If not, you can just remove the type prop from Button and specify <button type="button" /> explicitly:

export const Button = ({ className, ...props }) => {
  Button.defaultProps = { className: '' };

  return (
    <button
      className={['button', className].join(' ')}
      type="button"
      {...props}
    />
  );
};

@ljharb
Copy link
Member

ljharb commented Oct 8, 2020

@musicMan1337

export const Button = ({ className, type, ...props }) => {
  if (type === 'button') {
    return (
      <button
        className={['button', className].join(' ')}
        type="button"
        {...props}
      />
    );
  }
  if (type === 'submit') {
    return (
      <button
        className={['button', className].join(' ')}
        type="submit"
        {...props}
      />
    );
  }
};
Button.propTypes = {
  className: PropTypes.string,
  type: PropTypes.oneOf(['button', 'submit']),
};
Button.defaultProps = { className: '', type: 'button' };

There is never a need to dynamically specify a Button's type (and you never ever want a "reset" button, so those two are sufficient).

@musicMan1337
Copy link

To preface, I'm not the best with forms!

@Hypnosphi I've found in React I tend to only use submit buttons when I actually need a real button. If I'm just testing functionality with dummy buttons then it doesn't matter, then again I'm sure you can do wonderous effects using buttons!

@ljharb It's interesting how you feel about the "reset" type, I'll have to look into that some more since I have used them in other small projects, so thanks for the nod! And as for the "if" statement, I definitely see the logic, but I personally find it very cumbersome to repeat code in that way, particularly from a readability perspective.

Also, I didn't know you could put default props outside of the component, I'll have to play with that, thanks!

@ljharb
Copy link
Member

ljharb commented Oct 9, 2020

@musicMan1337 you can only defaultProps outside the component. Your statement doesn’t take effect until after the component has been rendered.

@musicMan1337
Copy link

@ljharb Oh nice that's good to know! I was being misdirected by my linter since the error turns off both ways (I guess in that specific case it just scrubs the whole file without scope?).

Definitely need more practice with React, most of my time has been spent on backend stuff. Also I now clearly understand the taboo of the "reset" button type!

@skriems
Copy link

skriems commented Mar 24, 2021

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  /**
   * enforcing the type attribute to be required and limited to "submit" and "button"
   */
  type: 'submit' | 'button';
};

export const Button = (props: ButtonProps) => {
  // see ButtonProps
  // eslint-disable-next-line react/button-has-type
  return <button {...props} />;
};

const Btn = () => <Button />; // <- ts error

my 2cents on this.

@H0JLuk

This comment was marked as spam.

@wallace-sf
Copy link

It worked for me:

import { type ButtonHTMLAttributes, type FC } from 'react';

export const Button: FC<ButtonHTMLAttributes<HTMLButtonElement>> = ({
  children,
  ...props
}) => {
  return (
    <button type="button" {...props} className="myCustomClass">
      {children}
    </button>
  );
};

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

No branches or pull requests