Index Of /
How to keep `this` pointing to the Stencil component with a DOM event?
Otober 18, 2020
I'm continuing to play with Stencil.js. This time I'm working with forms. One of the main things we can do with any form is submitting the data. And because we are not going to reload the page, we have to intercept the submit event and do something with the data in the Stencil-component.
Here's my first attempt, which is actually, working correctly:
export class SampleForm {
@State() formData = {};
private handleSubmit(evt: Event) {
evt.preventDefault();
console.log(this.formData);
}
render() {
return (
<form onSubmit={(evt) => this.handleSubmit(evt)}>
...
</form>
)
}
}
This piece of code is working very simple: function handleSubmit
is receiving the submit event, cancels it and then displaying the data from the form fields (something not important in this case).
The problem
Linter displayed an error:
warning JSX props should not use arrow functions react/jsx-no-bind
The reason behind it is the anonymous function inside the attribute. On each render of the component, the function will be created again and again. And that's not cool. That could impact performance because JS garbage collector will have to clear all these functions again and again.
At first sight, it can be fixed relatively easy. Since all that the anonymous function is doing is to get the event and send it to the submit function. So I should just set the end function as a param:
<form onSubmit={this.handleSubmit}>
And that's it! Isn't it?
Lost context
Unfortunately no. Nothing was working as expected. I found that this.formData
was not defined, because the context, this
, points at the <form>
element that was submitted and not the component where the function handleSubmit
is defined. So there is no access to the other component properties like formData
.
As I wrote in the lead for this article — I've been working with React's function components for too long — there is no problem with the context of the functions with events like this. But with Stencil, we have a normal DOM-event and to keep this
pointing at our component we should use an arrow function or try to bind the context. Surprise surprise, we are getting react/jsx-no-bind
warning when we use .bind()
.
Solution
Many articles are suggesting to use an arrow function inside the attribute to keep the context, but that was the primary problem. And the solution is just to use an arrow function when creating the function in the component:
private handleSubmit = (evt: Event) => {
...
}
This way when the function is attached to the component, the context of the execution will be the — component where the function was created.
And one more thing: how to pass custom arguments
It's easy when you deal with an event like onSubmit
with a single DOM event, but often, calling functions on events like onClick
, you need to pass some parameter. Usually, it looks like this:
<button onClick={() => buttonPress(button.id)}>{ button.label }</button>
To avoid react/jsx-no-bind
error in this case in React you can create another component and pass button.id
as a parameter. But that's React and new component means just another function. With Stencil, it's a bot more complicated because we'll have to create a new web-component and register it to the browser and it will be executed and... but wait. We are still using JSX. And we still can use just a function to create HTML-elements. And we don't have to create new Stencil components for that. Just something like this:
export class SampleForm {
...
private buttonPress = (id: string) => {
console.log(`button with ${id} was pressed`);
}
private Button = (props: { [key: string]: any }) => {
const click = () => this.buttonPress(props.id);
return <button onClick={click}>{ props.label }</button>;
}
render() {
return (
...
<Button label={button.label} id={button.id} />
...
)
}
}
HTH
Some frontender thoughts
Twitter,
GitHub,
LinkedIn