Stimulus outlets for inter-controller communication

Jordan Lee Jan 2026
3 tabs
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "results"]
  static outlets = ["results-list"]
  static values = {
    url: String
  }

  connect() {
    this.timeout = null
  }

  async search() {
    clearTimeout(this.timeout)

    const query = this.inputTarget.value.trim()

    if (query.length < 2) {
      this.clearResults()
      return
    }

    this.timeout = setTimeout(async () => {
      this.showLoading()

      const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`, {
        headers: { "Accept": "text/vnd.turbo-stream.html" }
      })

      if (response.ok) {
        const html = await response.text()
        // Communicate with the results list controller
        if (this.hasResultsListOutlet) {
          this.resultsListOutlet.update(html)
        }
      }
    }, 300)
  }

  showLoading() {
    if (this.hasResultsListOutlet) {
      this.resultsListOutlet.showLoading()
    }
  }

  clearResults() {
    if (this.hasResultsListOutlet) {
      this.resultsListOutlet.clear()
    }
  }
}
3 files · javascript, erb Explain with highlit

Outlets allow Stimulus controllers to reference and communicate with other controller instances, enabling composition without tight coupling. I define outlets by specifying which controller types to connect to, and Stimulus automatically finds matching controllers in the DOM. This pattern works well for coordinating behavior across components: a form controller might communicate with a modal controller, or a search controller with a results controller. Outlets provide typed references and callbacks when outlets connect or disconnect, making it easy to sync state. This is more maintainable than using custom events for every interaction, though events still have their place for loosely coupled scenarios.