Rails Stimulus e Importmaps em 2026: JavaScript Moderno Sem Ferramentas de Build

Guia completo sobre Rails Stimulus e Importmaps em 2026. Aprenda a criar aplicações JavaScript modernas sem webpack ou bundlers complexos usando Hotwire.

Rails Stimulus e Importmaps em 2026: JavaScript Moderno Sem Ferramentas de Build

O desenvolvimento JavaScript tradicionalmente exigiu configurações complexas, bundlers sofisticados e tempos de compilação prolongados. O Rails 8 em 2026 oferece uma abordagem radicalmente diferente com Stimulus e Importmaps, permitindo criar aplicações interativas sem nenhuma ferramenta de build.

Stimulus e Importmaps vêm incluídos por padrão no Rails 8. Essa combinação permite desenvolver interfaces ricas mantendo a simplicidade que caracteriza a filosofia do Ruby on Rails.

Entendendo Importmaps no Rails 8

Importmaps é uma tecnologia nativa dos navegadores modernos que permite importar módulos JavaScript diretamente através de URLs, sem necessidade de compilação prévia. O Rails integra essa funcionalidade de forma transparente.

O arquivo de configuração está localizado na raiz do projeto:

ruby
# config/importmap.rb
pin "application"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"

Essa configuração indica ao Rails quais módulos JavaScript estão disponíveis e onde encontrá-los. O comando pin_all_from é particularmente útil pois registra automaticamente todos os controllers Stimulus presentes na pasta especificada.

Para adicionar uma biblioteca externa a partir de um CDN, utiliza-se o seguinte comando:

bash
bin/importmap pin lodash

O Rails baixa automaticamente o pacote do jspm.io e o adiciona ao arquivo de configuração.

Arquitetura do Stimulus

O Stimulus é baseado em um conceito simples mas poderoso: os controllers. Cada controller é uma classe JavaScript que se conecta ao HTML através de atributos data. Essa abordagem mantém uma separação clara entre comportamento e apresentação.

A seguir está a estrutura de um controller Stimulus padrão:

app/javascript/controllers/search_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "results", "loading"]
  static values = {
    url: String,
    debounce: { type: Number, default: 300 }
  }

  connect() {
    console.log("Search controller connected")
  }

  search() {
    clearTimeout(this.timeout)
    this.timeout = setTimeout(() => {
      this.performSearch()
    }, this.debounceValue)
  }

  async performSearch() {
    const query = this.inputTarget.value
    if (query.length < 2) return

    this.loadingTarget.classList.remove("hidden")

    try {
      const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`)
      const html = await response.text()
      this.resultsTarget.innerHTML = html
    } finally {
      this.loadingTarget.classList.add("hidden")
    }
  }
}

O HTML correspondente utiliza atributos data para estabelecer a conexão:

erb
<div data-controller="search" data-search-url-value="/search">
  <input
    type="text"
    data-search-target="input"
    data-action="input->search#search"
    placeholder="Pesquisar..."
  />
  <div data-search-target="loading" class="hidden">Carregando...</div>
  <div data-search-target="results"></div>
</div>

Gerenciamento de Eventos com Actions

O sistema de actions do Stimulus permite vincular eventos DOM a métodos dos controllers de forma declarativa. A sintaxe segue o formato evento->controller#método.

app/javascript/controllers/form_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["submit", "error"]
  static values = { submitting: Boolean }

  connect() {
    this.submittingValue = false
  }

  async submit(event) {
    event.preventDefault()
    if (this.submittingValue) return

    this.submittingValue = true
    this.submitTarget.disabled = true
    this.errorTarget.textContent = ""

    const formData = new FormData(this.element)

    try {
      const response = await fetch(this.element.action, {
        method: "POST",
        body: formData,
        headers: {
          "Accept": "text/vnd.turbo-stream.html"
        }
      })

      if (!response.ok) {
        throw new Error("Erro ao enviar o formulário")
      }

      const html = await response.text()
      Turbo.renderStreamMessage(html)
    } catch (error) {
      this.errorTarget.textContent = error.message
    } finally {
      this.submittingValue = false
      this.submitTarget.disabled = false
    }
  }

  submittingValueChanged() {
    this.submitTarget.textContent = this.submittingValue
      ? "Enviando..."
      : "Enviar"
  }
}
erb
<%= form_with url: messages_path, data: { controller: "form" } do |f| %>
  <div data-form-target="error" class="text-red-600"></div>

  <%= f.text_area :content, required: true %>

  <%= f.submit "Enviar",
    data: {
      form_target: "submit",
      action: "click->form#submit"
    }
  %>
<% end %>

Comunicação Entre Controllers

Aplicações complexas frequentemente requerem que múltiplos controllers se comuniquem entre si. O Stimulus oferece várias abordagens para gerenciar essas interações.

A primeira abordagem utiliza eventos customizados do JavaScript:

app/javascript/controllers/cart_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["count", "total"]
  static values = { items: Array }

  connect() {
    this.element.addEventListener("cart:item-added", this.handleItemAdded.bind(this))
  }

  disconnect() {
    this.element.removeEventListener("cart:item-added", this.handleItemAdded.bind(this))
  }

  handleItemAdded(event) {
    const { item } = event.detail
    this.itemsValue = [...this.itemsValue, item]
    this.updateDisplay()
  }

  updateDisplay() {
    this.countTarget.textContent = this.itemsValue.length
    const total = this.itemsValue.reduce((sum, item) => sum + item.price, 0)
    this.totalTarget.textContent = `R$ ${total.toFixed(2)}`
  }
}
app/javascript/controllers/product_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { id: Number, name: String, price: Number }

  addToCart() {
    const event = new CustomEvent("cart:item-added", {
      bubbles: true,
      detail: {
        item: {
          id: this.idValue,
          name: this.nameValue,
          price: this.priceValue
        }
      }
    })
    this.element.dispatchEvent(event)
  }
}

Integração com Turbo

O Stimulus funciona em conjunto com o Turbo, o outro componente principal do Hotwire. Essa sinergia permite criar aplicações altamente reativas enquanto minimiza o JavaScript necessário.

app/javascript/controllers/infinite_scroll_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["entries", "pagination"]
  static values = { loading: Boolean }

  connect() {
    this.observer = new IntersectionObserver(
      entries => this.handleIntersection(entries),
      { rootMargin: "100px" }
    )

    if (this.hasPaginationTarget) {
      this.observer.observe(this.paginationTarget)
    }
  }

  disconnect() {
    this.observer.disconnect()
  }

  async handleIntersection(entries) {
    const entry = entries[0]
    if (!entry.isIntersecting || this.loadingValue) return

    const nextPageLink = this.paginationTarget.querySelector("a[rel='next']")
    if (!nextPageLink) return

    this.loadingValue = true

    try {
      const response = await fetch(nextPageLink.href, {
        headers: { "Accept": "text/vnd.turbo-stream.html" }
      })
      const html = await response.text()
      Turbo.renderStreamMessage(html)
    } finally {
      this.loadingValue = false
    }
  }
}

O controller Rails correspondente retorna Turbo Stream:

ruby
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.order(created_at: :desc).page(params[:page])

    respond_to do |format|
      format.html
      format.turbo_stream
    end
  end
end
erb
<%# app/views/articles/index.turbo_stream.erb %>
<%= turbo_stream.append "articles" do %>
  <%= render @articles %>
<% end %>

<%= turbo_stream.replace "pagination" do %>
  <div id="pagination">
    <%= link_to_next_page @articles, "Mais artigos", data: { turbo: false } %>
  </div>
<% end %>

Criação de Controllers Reutilizáveis

A modularidade é essencial para manter uma base de código limpa. Os controllers Stimulus podem ser projetados para serem altamente configuráveis através de values.

app/javascript/controllers/clipboard_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["source", "button"]
  static values = {
    successMessage: { type: String, default: "Copiado!" },
    errorMessage: { type: String, default: "Erro" },
    resetDelay: { type: Number, default: 2000 }
  }

  async copy() {
    const text = this.sourceTarget.value || this.sourceTarget.textContent

    try {
      await navigator.clipboard.writeText(text)
      this.showFeedback(this.successMessageValue, "success")
    } catch {
      this.showFeedback(this.errorMessageValue, "error")
    }
  }

  showFeedback(message, type) {
    const originalText = this.buttonTarget.textContent
    this.buttonTarget.textContent = message
    this.buttonTarget.classList.add(`feedback-${type}`)

    setTimeout(() => {
      this.buttonTarget.textContent = originalText
      this.buttonTarget.classList.remove(`feedback-${type}`)
    }, this.resetDelayValue)
  }
}
erb
<div data-controller="clipboard"
     data-clipboard-success-message-value="Copiado para a área de transferência!">
  <code data-clipboard-target="source">npm install @hotwired/stimulus</code>
  <button data-clipboard-target="button"
          data-action="clipboard#copy">
    Copiar
  </button>
</div>

Testes de Controllers Stimulus

Os testes garantem a confiabilidade dos controllers. Existem várias abordagens possíveis, incluindo Jest ou os testes de sistema do Rails.

test/javascript/controllers/clipboard_controller.test.jsjavascript
import { Application } from "@hotwired/stimulus"
import ClipboardController from "controllers/clipboard_controller"

describe("ClipboardController", () => {
  let application

  beforeEach(() => {
    document.body.innerHTML = `
      <div data-controller="clipboard">
        <input data-clipboard-target="source" value="test content" />
        <button data-clipboard-target="button" data-action="clipboard#copy">
          Copy
        </button>
      </div>
    `

    application = Application.start()
    application.register("clipboard", ClipboardController)
  })

  afterEach(() => {
    application.stop()
  })

  test("copies text to clipboard on click", async () => {
    const writeText = jest.fn().mockResolvedValue(undefined)
    Object.assign(navigator, {
      clipboard: { writeText }
    })

    const button = document.querySelector("[data-action]")
    button.click()

    await new Promise(resolve => setTimeout(resolve, 0))

    expect(writeText).toHaveBeenCalledWith("test content")
  })
})

Otimização de Performance

Embora os Importmaps eliminem a necessidade de bundling, algumas otimizações continuam sendo relevantes para aplicações em produção.

A configuração do cache HTTP é fundamental:

ruby
# config/environments/production.rb
config.public_file_server.headers = {
  "Cache-Control" => "public, max-age=31536000, immutable"
}

O preload de módulos críticos melhora o tempo de carregamento inicial:

erb
<%# app/views/layouts/application.html.erb %>
<head>
  <%= javascript_importmap_tags %>
  <%= preload_link_tag "application", as: "script", type: "module" %>
</head>

Para módulos externos volumosos, recomenda-se o carregamento lazy:

ruby
# config/importmap.rb
pin "chart.js", preload: false
app/javascript/controllers/chart_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  async connect() {
    const { Chart } = await import("chart.js")
    this.initializeChart(Chart)
  }

  initializeChart(Chart) {
    // Configuração do gráfico
  }
}

Pronto para mandar bem nas entrevistas de Ruby on Rails?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Conclusão

Rails Stimulus e Importmaps representam uma evolução significativa na forma de construir interfaces web interativas. Ao eliminar a complexidade das ferramentas de build tradicionais, essa abordagem permite que desenvolvedores se concentrem na criação de funcionalidades em vez da configuração do ambiente de desenvolvimento.

A combinação de Stimulus para comportamento JavaScript e Turbo para atualizações dinâmicas oferece uma alternativa poderosa aos frameworks JavaScript pesados. Essa arquitetura respeita os princípios fundamentais do Rails: convenção sobre configuração e melhoria progressiva do HTML.

Para equipes que buscam uma solução sustentável e de alta performance sem a complexidade dos bundlers modernos, Stimulus e Importmaps constituem uma opção relevante em 2026. A curva de aprendizado reduzida e a integração nativa com Rails os tornam uma opção particularmente atrativa para projetos de qualquer tamanho.

Desafio do dia

Você saberia encontrar o bug em Ruby on Rails?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 29 de agosto de 2026

Compartilhar

Artigos relacionados