# 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.
- Published: 2026-08-29
- Updated: 2026-08-29
- Author: Anthony Fillion-Maillet
- Reading time: 8 min
---
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:
```javascript
// app/javascript/controllers/search_controller.js
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
Carregando...
```
## 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`.
```javascript
// app/javascript/controllers/form_controller.js
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| %>
<%= 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:
```javascript
// app/javascript/controllers/cart_controller.js
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)}`
}
}
```
```javascript
// app/javascript/controllers/product_controller.js
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.
```javascript
// app/javascript/controllers/infinite_scroll_controller.js
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 %>
<% 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.
```javascript
// app/javascript/controllers/clipboard_controller.js
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
npm install @hotwired/stimulus
```
## 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.
```javascript
// test/javascript/controllers/clipboard_controller.test.js
import { Application } from "@hotwired/stimulus"
import ClipboardController from "controllers/clipboard_controller"
describe("ClipboardController", () => {
let application
beforeEach(() => {
document.body.innerHTML = `
`
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 %>
<%= javascript_importmap_tags %>
<%= preload_link_tag "application", as: "script", type: "module" %>
```
Para módulos externos volumosos, recomenda-se o carregamento lazy:
```ruby
# config/importmap.rb
pin "chart.js", preload: false
```
```javascript
// app/javascript/controllers/chart_controller.js
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
}
}
```
## 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.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/pt/blog/ruby-on-rails/rails-stimulus-importmaps-modern-javascript-without-build-tools