-
Notifications
You must be signed in to change notification settings - Fork 76
jsx-no-lambda and passing args in event handlers #96
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
Comments
You can do |
|
@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 |
@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. |
@StokeMasterJack rendering in a loop is really different case which is hard to guess from first post :)
IMHO Not sure that there is anything than can (or should) be corrected in this rule. |
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 |
@mohsen1 but you still have a lambda as a jsx prop. Thus you will still get the warning, right? |
@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}/>) |
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? |
@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 |
@StokeMasterJack I may be wrong on this, but with @IllusionMH example, you only instantiate the component once. When you use Though, creating a new component is more boilerplate than having to write a new component class... |
In ES6 you should be able to use currying, eg:
|
@johnsimons that's simply syntactic sugar. you are still creating a new lambda every time you call |
@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:
Do this:
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. |
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 |
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. |
bad pattern or good pattern in this case is very subjective. |
I got @mohsen1 solution working with I also got this working when passing deleteClick through as a prop Not sure if it's best practice, but it certainly got me running again |
@andrewjboyd 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 😉 |
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 |
How about exploiting |
Just add "jsx-no-lambda": false in your tslint.json under the rules. Below is the snippet of my tslint.json
|
I use a high-order component to resolve the issue. 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. |
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 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>
)
}
} |
@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.
|
Ignore "jsx-no-lambda" and disable it in your entire project by adding this to your tslint.json:
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. |
@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? |
Also, how would you deal with this when using SFCs ? there's no this and also no .bind |
Function components doesn't have Probably this rule should be enabled by default only for UPD. Under enabled for I mean to show error only of arrow function is prop of @dacevedo12 if Please correct me if I'm wrong. |
Hi all. In this article the author has a good approach to solve this issue: I hope this will be useful for all. |
@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. |
@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 ;) |
@cytrowski Could you provide an example of memoization for the handler makers? Do you mean you're wrapping the entire component in |
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>
)
}
)
}
} |
@cytrowski Ah, got it, thank you! But is that really worth the trouble? Why not just wrap the component inside |
As far as I know |
@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. |
@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… |
@haggholm - that's true :) But for that case I suggest to embrace event delegation and add event listener on the parent of that list. |
Seems the reason of issue is in definition of callback function on every render iteration. 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 }/>
}
} |
@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 ;) |
@cytrowski especially since React seems to embrace the idea with Hooks. At least based on their documentation. |
So many things have been said here, not sure if this idea has already been suggested (didn't find it when searching for it): PS: For all the people that don't like this rule at all, just disable it in you config... |
@johnsimons actually to work it should be vice versa:
|
|
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… |
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. |
Closing due to deprecation, see #210 |
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) }/>
The text was updated successfully, but these errors were encountered: