Refactor Stimulus.js Controllers to Use Change Callbacks

Stimulus’ values API has a great change callback function. This allows you to respond whenever a value changes. 🦉

For a quick refresher: the values API allows you read (and write!) data attributes to the controller’s element.

You define them like so in the HTML: <div data-controller="counter" data-counter-time-value="10">10</div>.

Then in the controller define them in the static values:

export default class extends Controller {
  static values = { time: Number }
}

With above controller you might expect a connect function to start the timer.

// app/javascripts/controllers/counter_controller.js
export default class extends Controller {
  static values = { time: Number }

  connect() {
    this.timer = setInterval(() => {
      this.timeValue--;

      if (this.timeValue > 0) {
        this.element.textContent = this.timeValue;
      } else {
        this.element.textContent = "Time's up!"

        clearInterval(this.timer);
      }
    }, 1000);
  }
}

And above might look just fine to you. But I like to keep my functions smaller in scope, and I am pretty sure the next requirement is to add a stop and pause feature (and I need a bridge to show how the changes callbacks can be used).

So let’s do a little refactor using Change Callbacks.

export default class extends Controller {
  static values = { time: Number }

  connect() {
    this.timer = null;

    this.#start();
  }

  // private

  timeValueChanged() {
    if (this.timeValue > 0) {
      this.element.textContent = this.timeValue;
    } else {
      this.element.textContent = "Time's up!";

      clearInterval(this.timer);
    }
  }

  #start() {
    this.timer = setInterval(() => {
      this.timeValue--;
    }, 1000);
  }
}

timevalueChanged() is called whenever the this.timeValue is changed (which is done in #start().

Is this better? “The class has gotten bigger!” But I’d argue it is better. Yes, significantly more lines of code, but to me it’s easier to follow. The #start function is only concerned with starting the timer. You can imagine there being a public function, alongside a stop() and pause() function.

  • show a maximum allowed character count;
  • fetch API endpoint when urlValue changes;
  • show the selected option from a select-element;
  • disable a button on “loading” value.

It is one of those, somewhat, hidden helper functions in Stimulus that if you know it, you see ways to use it in many situations.

How have you used change callbacks in Stimulus controllers? Let me know about them.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built for Ruby on Rails (inc. Rails 8)

  • Designed with Tailwind CSS and Enhanced with Hotwire

  • Updates for 12 months