How to keep `this` pointing to the Stencil component with a DOM event?

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 displays 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 JavaScript's garbage collector will have to clear all these functions again and again.

At first sight, it can be fixed easily. 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.

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 parameters. Usually, it looks like this:

<button onClick={() => buttonPress(button.id)}>{ button.label }</button>

To avoid the 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