# Rails Stimulus e Importmaps en 2026: JavaScript Moderno Sin Herramientas de Build
> Guía completa sobre Rails Stimulus e Importmaps en 2026. Aprende a crear aplicaciones JavaScript modernas sin webpack ni bundlers complejos usando Hotwire.
- Published: 2026-08-29
- Updated: 2026-08-29
- Author: Anthony Fillion-Maillet
- Reading time: 8 min
---
El desarrollo JavaScript tradicionalmente ha requerido configuraciones complejas, bundlers sofisticados y tiempos de compilación prolongados. Rails 8 en 2026 ofrece un enfoque radicalmente diferente con Stimulus e Importmaps, permitiendo crear aplicaciones interactivas sin ninguna herramienta de build.
> Stimulus e Importmaps vienen incluidos por defecto en Rails 8. Esta combinación permite desarrollar interfaces ricas manteniendo la simplicidad que caracteriza la filosofía de Ruby on Rails.
## Entendiendo Importmaps en Rails 8
Importmaps es una tecnología nativa de los navegadores modernos que permite importar módulos JavaScript directamente mediante URLs, sin necesidad de compilación previa. Rails integra esta funcionalidad de manera transparente.
El archivo de configuración se encuentra en la raíz del proyecto:
```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"
```
Esta configuración indica a Rails qué módulos JavaScript están disponibles y dónde encontrarlos. El comando `pin_all_from` resulta particularmente útil ya que registra automáticamente todos los controladores Stimulus presentes en la carpeta especificada.
Para agregar una biblioteca externa desde un CDN, se utiliza el siguiente comando:
```bash
bin/importmap pin lodash
```
Rails descarga automáticamente el paquete desde jspm.io y lo agrega al archivo de configuración.
## Arquitectura de Stimulus
Stimulus se basa en un concepto simple pero potente: los controladores. Cada controlador es una clase JavaScript que se conecta al HTML mediante atributos data. Este enfoque mantiene una separación clara entre el comportamiento y la presentación.
A continuación se muestra la estructura de un controlador Stimulus estándar:
```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")
}
}
}
```
El HTML correspondiente utiliza atributos data para establecer la conexión:
```erb
Cargando...
```
## Manejo de Eventos con Actions
El sistema de acciones de Stimulus permite vincular eventos DOM a métodos de los controladores de manera declarativa. La sintaxis sigue el formato `evento->controlador#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("Error al enviar el formulario")
}
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 %>
```
## Comunicación Entre Controladores
Las aplicaciones complejas frecuentemente requieren que múltiples controladores se comuniquen entre sí. Stimulus ofrece varios enfoques para manejar estas interacciones.
El primer enfoque utiliza eventos personalizados de 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 = `$${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)
}
}
```
## Integración con Turbo
Stimulus funciona en conjunto con Turbo, el otro componente principal de Hotwire. Esta sinergia permite crear aplicaciones altamente reactivas mientras se minimiza el JavaScript necesario.
```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
}
}
}
```
El controlador Rails correspondiente 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 %>
```
## Creación de Controladores Reutilizables
La modularidad es esencial para mantener un código base limpio. Los controladores Stimulus pueden diseñarse para ser altamente configurables mediante valores.
```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: "Error" },
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
```
## Pruebas de Controladores Stimulus
Las pruebas garantizan la confiabilidad de los controladores. Existen varios enfoques posibles, incluyendo Jest o las pruebas de sistema de 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")
})
})
```
## Optimización del Rendimiento
Aunque los Importmaps eliminan la necesidad de bundling, algunas optimizaciones siguen siendo relevantes para aplicaciones en producción.
La configuración del caché HTTP es fundamental:
```ruby
# config/environments/production.rb
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=31536000, immutable"
}
```
La precarga de módulos críticos mejora el tiempo de carga inicial:
```erb
<%# app/views/layouts/application.html.erb %>
<%= javascript_importmap_tags %>
<%= preload_link_tag "application", as: "script", type: "module" %>
```
Para módulos externos voluminosos, se recomienda la carga diferida:
```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) {
// Configuración del gráfico
}
}
```
## Conclusión
Rails Stimulus e Importmaps representan una evolución significativa en la forma de construir interfaces web interactivas. Al eliminar la complejidad de las herramientas de build tradicionales, este enfoque permite a los desarrolladores enfocarse en la creación de funcionalidades en lugar de la configuración del entorno de desarrollo.
La combinación de Stimulus para el comportamiento JavaScript y Turbo para las actualizaciones dinámicas ofrece una alternativa potente a los frameworks JavaScript pesados. Esta arquitectura respeta los principios fundamentales de Rails: convención sobre configuración y mejora progresiva del HTML.
Para equipos que buscan una solución mantenible y de alto rendimiento sin la complejidad de los bundlers modernos, Stimulus e Importmaps constituyen una opción relevante en 2026. La curva de aprendizaje reducida y la integración nativa con Rails los convierten en una opción particularmente atractiva para proyectos de cualquier tamaño.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/es/blog/ruby-on-rails/rails-stimulus-importmaps-modern-javascript-without-build-tools