# 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
``` ## 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