---
title: Taking the most out of Stimulus.js
teaser: 'Coming from SPA frameworks, Stimulus.js might feel underwhelming or frustrating.
  Here are some guides to help us take the best out of it.

  '
tags: stimulus,hotwire,javascript
author: Matheus Richard
published_on: 2022-07-26
---

Stimulus.js is a modest JavaScript framework for the HTML we already have. It
doesn't aspire to take over the front-end, but it's there to help you add a
few JS sprinkles when needed. It is also really useful to wrap external
libraries and let the HTML be the source of truth for the front-end.

Here are some guidelines to get the most out of Stimulus.js in our apps.

## Prefer general-purpose controllers

It might be tempting to use Stimulus on a per-page/per-resource-controller
basis, but we should use general-purpose controllers as a rule of thumb.

For instance, imagine we have a Rails `PinsController`. In the `show` action, we
want to be able to copy a pin to the clipboard. So, we might write:

```js
// ❌ Don't
class PinsController extends Controller {
  copyPIN() {
    const text = document.querySelector('#user-pin').value;
    navigator.clipboard.writeText(text)
  }
}
```

There's nothing specific to this behavior that we need to limit it just to this
(hypothetical) Pins page. With a few changes, we could make it work for any
page.

```js
// ✅ Do
class ClipboardController extends Controller {
  static targets = ['source'];

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value);
  }
}
```

With that in mind, it's helpful to **think in terms of behaviors** in our apps.
Which kinds of behaviors do we want them to have? Examples of behaviors are
showing/hiding/toggling elements in the screen, focusing on inputs, sorting rows
in a table, displaying a tooltip or modal, etc. Create controllers and actions
for each behavior.

Following this sort of Unix philosophy really makes Stimulus shine. Once we get
a few controllers, it will be possible to build new features by composing the
current existing behaviors!

## Compose controllers with events

Say we have a controller that copies the value of an input to the clipboard:

```html
<script>
  class ClipboardController extends Controller {
    static targets = ['source'];

    copy() {
      navigator.clipboard.writeText(this.sourceTarget.value);
    }
  }
</script>

<div data-controller="clipboard">
  <label>
    PIN:
    <input type="text" name="pin" data-clipboard-target="source" readonly />
  </label>
  <button data-action="clipboard#copy">Copy to clipboard</button>
</div>
```

Imagine that, in some places, we want to add a flash message saying "Copied!"
when the copy action happens. We could change the clipboard controller to also
display flash messages, but that feels wrong. We might already have a controller
that displays flash messages, so how can we compose them? Instead of reaching
out for something like inheritance, or duplicating code, we can use [events].

```js
class ClipboardController extends Controller {
  static targets = ['source'];

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value);
    this.dispatch('copy', {detail: {content: this.sourceTarget.value}});
  }
}
```

<aside class="info">
  Stimulus controllers have this handy <code>dispatch</code> method. It
  automatically prefixes the event name with the controller name. In the
  example above, the dispatched event will be <code>clipboard:copy</code>.
</aside>

And now, we wire the two controllers together in HTML:

```html
<div data-controller="clipboard flash" data-action="clipboard:copy->flash#show" data-flash-message-param="Copied!">
  <!-- 👆 Note the data-action in this line -->
  <label>
    PIN:
    <input type="text" name="pin" data-clipboard-target="source" readonly />
  </label>
  <button data-action="clipboard#copy">Copy to clipboard</button>
</div>
```

With this approach, we not only avoided violating the [single-responsibility]
principle, but we were able to compose behaviors without writing new code.

[events]: https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events
[actions]: https://stimulus.hotwired.dev/reference/actions
[getcontrollerforelementandidentifier]: https://stimulus.hotwired.dev/reference/controllers#directly-invoking-other-controllers
[single-responsibility]: https://www.betterstimulus.com/solid/single-responsibility.html

## Wrap external libraries in controllers

Sometimes we might use external libraries to build a particular feature. For
example, we might want to use [Tippy.js] to add tooltips to elements on the
page.

The naive approach would be writing something like:

```html
<button id="some-button">Hover me</button>

<script>
  const button = document.getElementById('some-button');
  tippy(button, {
    content: "I'm a tooltip!",
  });
</script>
```

It's good to separate the behavior itself from its implementation. So, we can
wrap this in a Stimulus controller:

```js
class TooltipController extends Controller {
  static values = {message: String}; // We could receive a config object here, to customize the tooltip

  connect() {
    this.tippyInstance = tippy(this.element, this.messageValue);
  }

  disconnect() {
    this.tippyInstance.destroy();
  }
}
```

And the HTML would look like this:

```html
<button data-controller="tooltip" data-tooltip-message-value="I'm a tooltip!">Hover me</button>
```

This might seem a bit verbose, but it doesn't tie the implementation to one
specific library. If we ever need to swap the underlying tooltip package, we'll
have to change that in a single place! Yay! 🎉

[tippy.js]: https://atomiks.github.io/tippyjs/

## Use the values API to customize behavior

Say we have a Stimulus controller to debounce events:

```js
import {Controller} from '@hotwired/stimulus';
import debounced from 'debounced';

export default class extends Controller {
  initialize() {
    debounced.initialize({input: {wait: 250}});
  }
}
```

What if we want to debounce a different event or change the wait time?
Fortunately, Stimulus.js lets us customize our controllers with the [values
API].

```html
<script>
  import {Controller} from '@hotwired/stimulus';
  import debounced from 'debounced';

  export default class extends Controller {
    static values = {config: Object};

    initialize() {
      debounced.initialize(this.configValue);
    }
  }
</script>

<label>
  A debounced input
  <input
    data-controller="debounced"
    data-debounced-config-value="{input: {wait: 250}}"
    data-action="debounced:input->controller#doSomething"
  />
</label>
```

And if we want, it's even possible to provide a default value!

```html
<script>
  import {Controller} from '@hotwired/stimulus';
  import debounced from 'debounced';

  export default class extends Controller {
    static values = {
      config: {type: Object, default: {input: {wait: 250}}}, // 👈 default value
    };

    initialize() {
      debounced.initialize(this.configValue);
    }
  }
</script>

<label>
  A debounced input
  <input
    id="debounced-input"
    data-controller="debounced"
    data-action="debounced:input->controller#doSomething"
  />
</label>
```

Using the Values API to customize controllers is a powerful way to make them
more flexible and reusable.

[values api]: https://stimulus.hotwired.dev/reference/values

## Prefer using actions to manually adding event listeners

Imagine a controller that allows us to trap focus within a particular HTML
element. Here's a possible implementation:

```html
<script>
  // ❌ Don't
  import {Controller} from '@hotwired/stimulus';

  export default class extends Controller {
    connect() {
      this.element.addEventListener('toggle', this.trapFocus);
    }

    disconnect() {
      this.element.removeEventListener('toggle', this.trapFocus);
    }

    trapFocus = event => {
      // ...
    };
  }
</script>

<details data-controller="menu">
  <summary>Open the menu</summary>
  <!-- ... -->
</details>
```

While this works, manually adding event listeners is an unnecessary pain. With
[actions] we can achieve the same thing with less code!

```html
<script>
  // ✅ Do
  import {Controller} from '@hotwired/stimulus';

  export default class extends Controller {
    trapFocus(event) {
      // ...
    }
  }
</script>

<details data-controller="menu" data-action="toggle->menu#trapFocus">
  <!-- 👆 Note the data-action in this line -->
  <summary>Open the menu</summary>
  <!-- ... -->
</details>
```

<aside class="warn">
  <p>
    Another reason to be careful with event listeners is that <strong>we have to
    remember to remove them</strong> so we don’t leak memory. You can do this using
    the <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters">{once: true}</a>
    option when creating the event listener or by manually removing it
    (usually on the <code>disconnect</code> method of your Stimulus controller).
  </p>

  <p>
    Generally, it's a good idea to rely on actions and use event listeners sparingly.
  </p>
</aside>

Furthermore, using actions to annotate the HTML helps us communicate the app's
intention and behavior.

## Decouple JS and CSS with the Classes API

We often have to change CSS classes from elements via JS. The problem is that
whenever we change those classes, we need to update the HTML and the Stimulus
controllers that use them. This might not be obvious when refactoring, and we
might end up with deprecated classes in the JS code!

So, instead of doing something like this:

```js
// app/javascript/controllers/toggler_controller.js
import {Controller} from '@hotwired/stimulus';

export default class extends Controller {
  static targets = ['element'];

  show() {
    this.elementTarget.classList.remove('hidden');
  }

  hide() {
    this.elementTarget.classList.add('hidden');
  }
}
```

We can use Stimulus [Classes API] to refer to CSS classes:

```js
// app/javascript/controllers/toggler_controller.js
// ✅ Do
import {Controller} from '@hotwired/stimulus';

export default class extends Controller {
  static targets = ['element'];
  static classes = ['hidden'];

  show() {
    this.elementTarget.classList.remove(this.hiddenClass);
  }

  hide() {
    this.elementTarget.classList.add(this.hiddenClass);
  }
}
```

And the template would look like this:

```html
<div data-controller="toggler" data-toggler-hidden-class="some-component--hidden">
  <span class="some-component" data-toggler-target="element">This toggles!</span>
  <button data-action="toggler#show">Show</button>
  <button data-action="toggler#hide">Hide</button>
</div>
```

<aside class="info">
Like the Values API, the Classes API helps the controller be more flexible. By changing the <code>hiddenClass</code>, we could do more than just hide/show elements!
</aside>

This technique is particularly beneficial when using a utility-first CSS library
like Tailwind.

```js
// ✅ Do
// app/javascript/controllers/toggler-controller.js
import {Controller} from '@hotwired/stimulus';

export default class extends Controller {
  static targets = ['element'];
  static classes = ['hidden'];

  show() {
    this.elementTarget.classList.remove(...this.hiddenClasses);
  }

  hide() {
    this.elementTarget.classList.add(...this.hiddenClasses);
  }
}
```

[classes api]: https://stimulus.hotwired.dev/reference/css-classes

## Stick with Vanilla.js

The fewer libraries we depend on, the easier are upgrades and refactors. Several
projects depend on [jQuery], for instance. While transitioning to Hotwire, we
might be able to rewrite some code not to rely on external libraries. Modern
JavaScript has many [goodies] and [excellent browser support], so let's take
advantage of that!

Here's a compilation of pure JS alternatives to jQuery:
<https://youmightnotneedjquery.com>.

[jquery]: https://jquery.com/
[goodies]: https://github.com/lukehoban/es6features
[excellent browser support]: https://caniuse.com/es6

## It's just sprinkles

Remember that [Turbo] is still the go-to approach to building reactive applications
in Rails. Stimulus is just the last 10-20% of the way. You [might not need JS at
all]! Make sure the way you're using it reflects that philosophy.

[turbo]: https://turbo.hotwired.dev/
[might not need js at all]: https://www.diogorodrigues.dev/blog/6-powerful-css-techniques-you-can-use-instead-javascript
