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

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

Closed
StokeMasterJack opened this issue May 14, 2017 · 51 comments
Closed

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

StokeMasterJack opened this issue May 14, 2017 · 51 comments

Comments

@StokeMasterJack
Copy link

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
Copy link
Contributor

mohsen1 commented May 14, 2017

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
Copy link
Author

  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
Copy link
Contributor

@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
Copy link
Author

@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
Copy link
Contributor

@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
Copy link
Contributor

mohsen1 commented May 15, 2017

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
Copy link
Author

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

@IllusionMH
Copy link
Contributor

@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
Copy link
Author

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
Copy link
Contributor

@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
Copy link

@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
Copy link

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

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

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

}

@adidahiya
Copy link
Contributor

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

@derrickbeining
Copy link

derrickbeining commented Mar 20, 2018

@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
Copy link

alamothe commented Apr 6, 2018

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
Copy link

vlaja commented May 17, 2018

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
Copy link

icenold commented Jun 14, 2018

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
Copy link

andrewjboyd commented Jun 18, 2018

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
Copy link

alamothe commented Jun 19, 2018

@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
Copy link

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
Copy link

Sharlaan commented Jul 8, 2018

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
Copy link

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 } }

tomclose added a commit to magmo/rps-poc that referenced this issue Aug 6, 2018
tomclose added a commit to magmo/rps-poc that referenced this issue Aug 6, 2018
@Bomber77
Copy link

Bomber77 commented Aug 9, 2018

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
Copy link

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
Copy link

vlaja commented Aug 15, 2018

@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
Copy link

jeff3dx commented Aug 27, 2018

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
Copy link

@jeff3dx - #96 (comment) - what is wrong with my example?

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

@dacevedo12
Copy link

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

@IllusionMH
Copy link
Contributor

IllusionMH commented Nov 1, 2018

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
Copy link

jeanbenitez commented Dec 19, 2018

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
Copy link

carpben commented Dec 24, 2018

@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
Copy link

cytrowski commented Dec 24, 2018

@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 ;)

lalexgap pushed a commit to magmo/apps that referenced this issue Jan 9, 2019
@jessepinho
Copy link

@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
Copy link

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
Copy link

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

@cytrowski
Copy link

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
Copy link

@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
Copy link

haggholm commented Mar 3, 2019

@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
Copy link

@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
Copy link

Webbrother commented Mar 6, 2019

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
Copy link

@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
Copy link

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

@karfau
Copy link

karfau commented Mar 13, 2019

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
Copy link

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
Copy link

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
Copy link

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
Copy link

jessepinho commented Jun 1, 2019

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.

@adidahiya
Copy link
Contributor

Closing due to deprecation, see #210

sunkibaek added a commit to sunkibaek/duck-frontend that referenced this issue Feb 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests