palantir / tslint-react

:orange_book: Lint rules related to React & JSX for TSLint.
Apache License 2.0
749 stars 76 forks source link

jsx-no-lambda and passing args in event handlers #96

Closed StokeMasterJack closed 4 years ago

StokeMasterJack commented 7 years ago

If one never uses lambdas in a jsx arg, how would one pass args to an event handler? For example:

<Foo onClick={ (event) => this.foo(23) }/>

mohsen1 commented 7 years ago

You can do onClick={this.foo.bind(this, 23)} but I would question why this is necessary. 23 should be either a prop or part of state.

StokeMasterJack commented 7 years ago
  1. I just hard-coded 23 as an example. Sorry. I thought that was obvious.
  2. Wouldn't bind produce a similar warning as it too is creating a new function with each render?
  3. I believe it would trigger the "jsx-no-bind" rule.
IllusionMH commented 7 years ago

@StokeMasterJack we are using class components with bindings in constructor in this case. React docs on handling events has same examples (see Toggle component example). In this case function is created and bound only once on instance and not on each re-render.

Moreover in TS you can use decorators for binding (e.g. first from search) or make own implementation like Office UI Fabric React did

StokeMasterJack commented 7 years ago

@IllusionMH I think you missed the main thing I am asking: about passing an arg to the event handler. The arg I am talking about is row-specific and part of a list. For example, in a list of customers like this like this:

customers.map( c => <Btn onClick={ () => this.deleteCust(c.id) } /> );

The only way I know of to pass the customer id (from the current row) to the deleteCust method is to either use a lambda (like in this example) or to use bind. Both techniques cause a new function object to be created on every render.

IllusionMH commented 7 years ago

@StokeMasterJack rendering in a loop is really different case which is hard to guess from first post :) 1) Use // tslint:disable-next-line jsx-no-lambda comment 2) Create class component which will use bound method (will be more useful Foo is larger that one button).

IMHO Not sure that there is anything than can (or should) be corrected in this rule.

mohsen1 commented 7 years ago

What I usually do is creating another component that accepts customer as prop.

function DeleteCostomerButton({customer, deleteCust}) {
  return <Btn onClick={() => deleteCust(customer)} />
}

that's more reacty

StokeMasterJack commented 7 years ago

@mohsen1 but you still have a lambda as a jsx prop. Thus you will still get the warning, right?

IllusionMH commented 7 years ago

@StokeMasterJack @mohsen1 as far as I know - this is only way to avoid function creation on each render in a loop, but still can be slower depending on actual structure

class DeleteByIdButton extends PureComponent {
    constructor(props) {
        super(props);
        this.handleDelete= this.handleDelete.bind(this) // remove  in case you are using decorators for binding
    }
    // @autobind  // uncomment in case you are using decorators for binding
    handleDelete() {
        this.props.onClick(this.props.id);
    }
    render() {
        return <Btn onClick={this.handleDelete}>Delete</Btn>
    }
}
// ...
users.map(u => <DeleteByIdButton id={u.id} onClick={deleteUserFunction} key={u.id}/>)
StokeMasterJack commented 7 years ago

Now you are creating a whole new component with each render in a loop. I'm not so convinced that this represents a best practice.

Here is my take-away (unless presented with some new information): In my opinion, using a lambda (or a bind) in a jsx property (i.e. for an event handler) is a completely valid and appropriate use-case in certain situations: particularly inside of a loop when you need to pass arguments.

Here is a StackOverflow thread on the subject: Are lambda in JSX attributes an anti-pattern?

adidahiya commented 7 years ago

@StokeMasterJack I agree that in practice, you sometimes have to use lambdas in JSX properties and the performance cost is negligible. Despite that, I still think this lint rule is useful because it guards against sloppy code patterns in the majority of cases. In the few cases where you need to circumvent the rule you can simply use a // tslint:disable flag.

bezreyhan commented 7 years ago

@StokeMasterJack I may be wrong on this, but with @IllusionMH example, you only instantiate the component once. When you use bind or lambdas you create n new lambdas on each render, n being the number of times you are looping. By creating a new component, you are also able to do further optimizations with shouldComponentUpdate.

Though, creating a new component is more boilerplate than having to write a new component class...

johnsimons commented 7 years ago

In ES6 you should be able to use currying, eg:

<Foo onClick={ this.foo(23) }/>

foo = (event) => (id) => {

}
adidahiya commented 7 years ago

@johnsimons that's simply syntactic sugar. you are still creating a new lambda every time you call this.foo.

derrickbeining commented 6 years ago

@StokeMasterJack (and @adidahiya , @IllusionMH , @mohsen1, @bezreyhan , @johnsimons ), I've been looking for an answer to this for a while. Found this thread while googling it (yet again). I'm not a TS user, but this is a general React concern, especially if trying to take advantage of React.PureComponent.

I've been trying to come up with a non-laborious and not-too-clever way of doing exactly what you're looking for: avoiding passing a function literal on props, particularly when rendering out components from a list of some data on state, where it almost feels impossible to avoid passing function expressions on props.

Here's the solution I've come up with (sans TS):

Instead of doing this:

class CatList extends React.Component {

  static propTypes = {
    kitties: PropTypes.arrayOf(PropTypes.instanceOf(Cat)),
  }

  render() {
    // ... other stuff ...

    // ... the problem:
    {this.props.kitties.map(kitty => {
      return <PureKittyCat key={kitty.id} onClick={ () => this.props.adoptKitty(kitty) } />

      // onClick is getting a new Function object every time CatList re-renders
      // causing PureKittyCat to re-render as well because its props have "changed".
      // This feels necessary bc we need to pass `kitty` to `this.props.adoptKitty`
    })}
  }
}

Do this:

class CatList extends React.Component {

  static propTypes = {
    kitties: PropTypes.arrayOf(PropTypes.instanceOf(Cat)),
  }

  // create a component-local cache to store instance methods you want to associate
  // with arbitrary entities on state. Use WeakMap to mitigate potential memory leaks:

  this.methodCache = new WeakMap()

  render() {
    // ... other stuff...

    // ... the good stuff:
    {this.props.kitties.map(kitty => {
      if ( !this.methodCache.has(kitty) ) {
        this.methodCache.set(kitty, {
          adopt: () => this.props.adoptKitty(kitty),
        })
      }
                          // as long is this is the same in-memory kitty, onClick will
                          // receive the same in-memory function object every render
                         // so prop won't change, avoiding a re-render in a PureComponent
      return <PureKittyCat key={kitty.id} onClick={this.methodCache.get(kitty).adopt} />
    })}
  }
}

I believe this is a viable solution, but if someone foresees a problem, please share how this may not work out the way I think it should. Or if anyone has a more straightforward approach, I'd love to hear alternatives. But I think this is way is pretty explicit and minimalistic.

Of course for this to work, you have to consistently follow the convention of treating all of your state immutably throughout your application. This could create some weird gotchas if you're ever mutating state/props directly.

alamothe commented 6 years ago

Unless this rule can detect a loop, it's meaningless - there are no workarounds which are more performant - in fact this seems a case where it creates the opposite effect (people trying to circumvent the rule create less performant and/or less readable code)

In fact, this rule is disabled in TypeScript-React-Starter

vlaja commented 6 years ago

The rule itself is still enabled inside Typescript:recommended, and for me it's one of the best rules out there, because it allows people who recently started to code react to avoid perf bottlenecks.

Using a lambda inside the render method is definitively a bad pattern.

When you use a lambda inside the render, it will be recreated each time when the component is re-rendered. Meaning, that it will be recreated on each state / prop change.

However, if lambda is created inside the class itself, as it's private mehtod it will be instanced once, and just reused / re-binded when it is used with this.handleSomething ....

This is usually not a problem, but can sometimes be a huge difference, especially with longer component trees, and it's not simply syntactic sugar stuff.

Also, considering that in each render a lambda is re-created it will always differ from the one in the last pass, meaning that component or a whole component tree will probably re-render far more often they required, which is probably why this rule was created in the first place.

Use:

@IllusionMH example

If this is not possible for any reason, you can also achieve passing data with datasets, for example on elements which don't have a value prop (which serialise a DOM string map), and passing callback functions from parent to child components.

icenold commented 6 years ago

bad pattern or good pattern in this case is very subjective. I'm not going to create another component just to render a simple table. Much more when my table has something like <td onClick="{()=>this.bar(item)}" >{item.name}<td> where <td> is inside a <tr> rendered by Array.map(..)

andrewjboyd commented 6 years ago

I got @mohsen1 solution working with <foo onClick={this.deleteClick.bind(this, id)} /> My deleteClick event looks like this (I am using TypeScript) private deleteClick(word:number) { ... }

I also got this working when passing deleteClick through as a prop <foo onClick={this.props.deleteClick.bind(this, id)} />

Not sure if it's best practice, but it certainly got me running again

alamothe commented 6 years ago

@andrewjboyd bind will create a lambda, so the "fix" is only to silence the linter, performance-wise it is the same as the vanilla version.

This goes back to how the main effect of this rule is to make people circumvent it by writing effectively the same (or worse) code in a different way, only to lose precious time in the process 😉

andrewjboyd commented 6 years ago

Fair call @alamothe... I'm new to React / TypeScript so I was very happy to finally see it working this morning, but I'll change my code to reflect the solution @IllusionMH put forward

Sharlaan commented 6 years ago

How about exploiting event bubbling with all your child methods stacked on the list's container component, then catching the clicked child's id through the passed event.target object ?

example

jameskuizon13 commented 6 years ago

Just add "jsx-no-lambda": false in your tslint.json under the rules. Below is the snippet of my tslint.json

{ "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], "linterOptions": { "exclude": ["config/**/*.js", "node_modules/**/*.ts"] }, "rules": { "interface-name": [true, "never-prefix"], "no-console": false, "jsx-no-lambda": false } }

Bomber77 commented 6 years ago

I use a high-order component to resolve the issue. I want to use map method to create sub component in Steps, it a component in ant design. like below.

const SomeComponent = ({stepList}) => {  
  const handleClick = () => {
     //do something.
  }
  return (
    <div>
      <Steps>
        {
           stepList.map(step => (
              <Step onClick={handleClick}/>
           ))
        }
      </Steps>
    </div>
  )
}

I add a new high-order component named "StepWithData" to pass step into handleClick function, like this:

const StepWithData = (props) => {
  const {step, onClick, ...rest } = props;
  const onClickWithData = () => {
    onClick(step);
  }
  return(
    <Step onClick={onClickWithData} {...rest} />
  )
};

const SomeComponent = ({stepList}) => {  
  const handleClick = () => {
     //do something.
  }
  return (
    <div>
      <Steps>
        {
           stepList.map(step => (
              <StepWithData step={step} onClick={handleClick}/>
           ))
        }
      </Steps>
    </div>
  )
}

It work for me.

cytrowski commented 6 years ago

Hi guys :) I believe this rule is a big overkill if we consider most rendering cases. I understand making it forbidden to use lambda in iteration since making function in a loop is a bad practice anyway but assuming every user interface is a target of constant updates and rendering of huge lists is too much :)

However - if we really want to be purists then why not use data- attributes and make it event more performant in terms of memory usage:

class List extends Component {
  state = {
    items: [ { id: 1, title: 'Fizz' }, { id: 2, title: 'Buzz' } ]
  }
  handleClick = event => {
    // dataset elements are strings - that's why we parseInt here - consider it a compromise :)
    const itemId = parseInt(event.target.dataset.itemId, 10)
    this.setState({ items: this.state.items.filter(item => item.id !== itemId })
  }
  render() {
    return (
      <ul>
        { 
          this.state.items.map(
            item => (
              <li 
                key={item.id} 
                data-item-id={item.id} 
                onClick={this.handleClick}
              >
                {item.title}
              </li>
           ) 
        }
      </ul>
    )
  }
}
vlaja commented 6 years ago

@cytrowski Sure, datasets are a way to go. This is a great example.

You can also skip calling parseInt if you use object destructuring as it will cast the id to int automatically.

  handleClick = event => {
    const { itemId } = event.target.dataset;
    this.setState({ items: this.state.items.filter(item => item.id !== itemId })
  }
jeff3dx commented 6 years ago

Ignore "jsx-no-lambda" and disable it in your entire project by adding this to your tslint.json:

"rules": {
    "jsx-no-lambda": [ false ]
}

None of the alternatives in this thread actually avoid the performance issue this rule tries to avoid, but only wack-a-mole, or even make it worse with unnecessary complexity. Unless you're coding a ray-tracer this is just not a problem. Don't be afraid of lambdas in JSX, or just quit your React job and go drive Uber.

cytrowski commented 6 years ago

@jeff3dx - https://github.com/palantir/tslint-react/issues/96#issuecomment-412999164 - what is wrong with my example?

@vlaja - I'm not sure you are right with the casting in destructuring - is it documented somewhere?

vlaja commented 6 years ago

@cytrowski It was a behaviour i observed in a multitude of my projects, but it might be something tied to Typescript (which I use on almost all of my projects) or my compiler configuration. I'll retest this and let you know.

As for the discussion in place. It seems that we have a lot of subjective opinions on the matter and I don't see it leading anywhere, as nobody is providing any real performance tests (shame on me aswell). For this reason I'm adding 2 articles that could shed some light on the matter.

Inline functions in React https://cdb.reacttraining.com/react-inline-functions-and-performance-bdff784f5578

Arrow function performance in class properties https://medium.com/@charpeni/arrow-functions-in-class-properties-might-not-be-as-great-as-we-think-3b3551c440b1

Both articles are property of their perspective authors, I just found them to widen my perception of the problem a bit, and even made me change my mind to this is a more of a case by case basis.

It seems to depend a lot on how many times the component actually updates and the fact that having simple events such as this.setState() as anonymous functions inline could make the bundle footprint smaller, which is often overlooked as a potential benefit.

janhesters commented 6 years ago

@cytrowski I tried your approach in React Native with TypeScript like this:

handlePress = (event: GestureResponderEvent) => {
    this.props.toggleEmotion(event.target.dataset.item);
  };

  renderItem = ({ item }: { item: Emotion }) => (
    <ListItem
      title={item.name}
      bottomDivider={true}
      checkmark={item.chosen}
      checkmarkColor={activeColor}
      data-item={item}
      onPress={this.handlePress}
    />
  );

But it does not work. It throws the error: [ts] Property 'dataset' does not exist on type 'number'.

cytrowski commented 6 years ago

@janhesters I'm not experienced with TS and ReactNative but it looks like data- attributes behave differently there. I believe the GestureResponderEvent should be generic so that you can specify what kind of component can trigger that. Here the typing system assumes that target is a number which is already weird here. In the browser you can do it.

dacevedo12 commented 6 years ago

Also, how would you deal with this when using SFCs ? there's no this and also no .bind

IllusionMH commented 6 years ago

Function components doesn't have shouldComponenUpdate lifecycle (if we aren't talking 16.7-alpha and hooks) and they will be re-rendered each time parent will be rendered (but underlying DOM won't be changed until it is necessary).

Probably this rule should be enabled by default only for PureComponent's where arrow functions will make default shouldComponentUpdate useless and will force constant re-renderings, but in case of SFC's it won't make any difference.

UPD. Under enabled for I mean to show error only of arrow function is prop of PureComponent.

@dacevedo12 if SFC is re-rendered each time than most likely render will be called on it's children. Unless you use PureComponent as child and pass arrow function as prop - it wont make any difference.

Please correct me if I'm wrong.

jeanbenitez commented 5 years ago

Hi all. In this article the author has a good approach to solve this issue:

https://medium.freecodecamp.org/why-arrow-functions-and-bind-in-reacts-render-are-problematic-f1c08b060e36

I hope this will be useful for all.

carpben commented 5 years ago

@cytrowski, though your solution is relevant and practical, personally I would avoid it. It kind of uses "truth in the dom", or at least it transfers data via the dom. The main reason we all use TS is to enable static type checking, which is lost once you transfer data via the dom. Besides the fact that it is considered a bad practice, conceptually I think of the dom the output of our JS/TS code, so it makes more sense to me to store information in actual JS/TS code.

cytrowski commented 5 years ago

@carpben than you for the feedback :) I agree and in the same time I consider my solution a "premature optimization". Right now (since my last response was in August) I use event handler makers with memoization on the component level which is much more readable and can be fully statically typed. It gives me the similar performance and I find it easier to reason about too.

BTW Merry X-mass ;)

jessepinho commented 5 years ago

@cytrowski Could you provide an example of memoization for the handler makers? Do you mean you're wrapping the entire component in React.memo?

cytrowski commented 5 years ago

Nope, it's much simpler than that :)

const memoize = (f, cache = {}) => x =>
  cache[x] ? cache[x] : (cache[x] = f(x))
class TaskList extends Component {
  // ...
  toggleTaskCompletion = memoize(taskId => () => doSthWithTask(taskId))
  // ...
  render() {
    return (
      <ul>
      {
         this.state.tasks.map( task => (
           <li 
             key={task.id}  
             onClick={toggleTaskCompletion(task.id)}
           >
             ....
           </li>
         )
      }
   )
  }

}
jessepinho commented 5 years ago

@cytrowski Ah, got it, thank you! But is that really worth the trouble? Why not just wrap the component inside React.memo(...)?

cytrowski commented 5 years ago

As far as I know .memo memoizes the whole component (works like a PureComponent class) so in this case it wont' help (fix me if I'm wrong). Anyway - I do use memoize only if the performance is really a thing and it's easy since you need to wrap only the "suboptimal" handler with it.

jessepinho commented 5 years ago

@cytrowski Ha, true — I guess I was thinking more of fixing the underlying performance problem, rather than just the linting issue. Honestly, the linting rule isn't probably the best. Been reading some articles (like this one from Ryan Florence) that seems to indicate there isn't much of a performance hit when using inline lambdas anyway.

haggholm commented 5 years ago

@cytrowski That’s a pretty neat solution, though it does come with a caveat: now all these allocated functions will sit around in your cache until the component is destroyed. Presumably not a problem in most cases, but in most cases these kinds of optimisations probably aren’t relevant anyway. On the other hand, if you have a big paginated table with millions of numerically indexed rows, you might end up with millions of cached lambdas…

cytrowski commented 5 years ago

@haggholm - that's true :) But for that case I suggest to embrace event delegation and add event listener on the parent of that list.

Webbrother commented 5 years ago

Seems the reason of issue is in definition of callback function on every render iteration. Just define callback in separate variable and use it.

const clickHandler = (event) => console.log('clicked');
<Foo onClick={clickHandler}/>

or, in case if you use class based component

class MyComponent extends Component {
  constructor() {
     super()
     this.clickHandler.bind(this)
  }
  foo(val) {
    console.log(val);
  }
  clickHandler() {
    this.foo(23)
  }
  render() {
    return <Foo onClick={ this.clickHandler }/>
  }
}
cytrowski commented 5 years ago

@Webbrother - we are talking about the case when you need an extra, non-event-based param during the call (eg. item id - for some REST API call).

I think with React Hooks the whole discussion here becomes irrelevant anyway ;)

SomethingSexy commented 5 years ago

@cytrowski especially since React seems to embrace the idea with Hooks. At least based on their documentation.

karfau commented 5 years ago

So many things have been said here, not sure if this idea has already been suggested (didn't find it when searching for it): For me it sounds like this rule could be changed to (optionally) only trigger inside class components.

PS: For all the people that don't like this rule at all, just disable it in you config...

fadykstas commented 5 years ago

ES6 you should be able to use currying, eg:


<Foo onClick={ this.foo(23) }/>

foo = (event) => (id) => {

}

@johnsimons  actually to work it should be vice versa:

<Foo onClick={ this.foo(23) }/>

foo = (id) => (event) => {

}

naijopkr commented 5 years ago

If one never uses lambdas in a jsx arg, how would one pass args to an event handler? For example:

<Foo onClick={ (event) => this.foo(23) }/>


myFunction => (var23) => (event) => console.log(var23, event)

<Foo onClick={myFunction(23)} />
haggholm commented 5 years ago

If one never uses lambdas in a jsx arg, how would one pass args to an event handler? For example: <Foo onClick={ (event) => this.foo(23) }/>

myFunction => (var23) => (event) => console.log(var23, event)

<Foo onClick={myFunction(23)} />

Ultimately, though, it’s just a bit less clear with no genuine improvement, as it’s still creating a new closure every iteration; it’s only that it doesn’t look like a lambda in the JSX…

jessepinho commented 5 years ago

If tslint weren’t being deprecated, I’d advocate for a PR to remove this rule entirely. It just causes confusion, there are no clear benefits to it, and it’s causing people to develop workarounds (currying) that have no actual impact on performance since they’re still creating new lambdas each time.

I think it’s time to stop trying to fix this rule and simply stop using it.