# Rails Stimulus et Importmaps en 2026 : JavaScript Moderne Sans Outils de Build > Guide complet sur Rails Stimulus et Importmaps en 2026. Découvrez comment créer des applications JavaScript modernes sans webpack ni bundlers complexes avec Hotwire. - Published: 2026-08-29 - Updated: 2026-08-29 - Author: Anthony Fillion-Maillet - Reading time: 8 min --- Le développement JavaScript a longtemps été synonyme de configurations complexes, de bundlers sophistiqués et de temps de compilation interminables. Rails 8 en 2026 propose une approche radicalement différente avec Stimulus et Importmaps, permettant de créer des applications interactives sans aucun outil de build. > Stimulus et Importmaps sont inclus par défaut dans Rails 8. Cette combinaison permet de développer des interfaces riches tout en conservant la simplicité qui fait la philosophie de Ruby on Rails. ## Comprendre Importmaps dans Rails 8 Importmaps est une technologie native des navigateurs modernes qui permet d'importer des modules JavaScript directement via des URLs, sans nécessiter de compilation préalable. Rails intègre cette fonctionnalité de manière transparente. Le fichier de configuration se trouve à la racine du projet : ```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" ``` Cette configuration indique à Rails quels modules JavaScript sont disponibles et où les trouver. La commande `pin_all_from` est particulièrement utile car elle enregistre automatiquement tous les contrôleurs Stimulus présents dans le dossier spécifié. Pour ajouter une bibliothèque externe depuis un CDN, la commande suivante est utilisée : ```bash bin/importmap pin lodash ``` Rails télécharge automatiquement le package depuis jspm.io et l'ajoute au fichier de configuration. ## Architecture de Stimulus Stimulus repose sur un concept simple mais puissant : les contrôleurs. Chaque contrôleur est une classe JavaScript qui se connecte au HTML via des attributs data. Cette approche maintient une séparation claire entre le comportement et la présentation. Voici la structure d'un contrôleur Stimulus standard : ```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") } } } ``` Le HTML correspondant utilise des attributs data pour établir la connexion : ```erb
``` ## Gestion des Événements avec Actions Le système d'actions de Stimulus permet de lier les événements DOM aux méthodes des contrôleurs de manière déclarative. La syntaxe suit le format `événement->contrôleur#méthode`. ```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("Erreur lors de la soumission") } 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 ? "Envoi en cours..." : "Envoyer" } } ``` ```erb <%= form_with url: messages_path, data: { controller: "form" } do |f| %>
<%= f.text_area :content, required: true %> <%= f.submit "Envoyer", data: { form_target: "submit", action: "click->form#submit" } %> <% end %> ``` ## Communication Entre Contrôleurs Les applications complexes nécessitent souvent que plusieurs contrôleurs communiquent entre eux. Stimulus propose plusieurs approches pour gérer ces interactions. La première approche utilise les événements personnalisés 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) } } ``` ## Intégration avec Turbo Stimulus fonctionne de concert avec Turbo, l'autre composant majeur de Hotwire. Cette synergie permet de créer des applications hautement réactives tout en minimisant le JavaScript nécessaire. ```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 } } } ``` Le contrôleur Rails correspondant retourne du 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 %> ``` ## Création de Contrôleurs Réutilisables La modularité est essentielle pour maintenir une base de code propre. Les contrôleurs Stimulus peuvent être conçus pour être hautement configurables via les valeurs. ```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: "Copié !" }, errorMessage: { type: String, default: "Erreur" }, 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
``` ## Tests des Contrôleurs Stimulus Les tests garantissent la fiabilité des contrôleurs. Plusieurs approches sont possibles, notamment avec Jest ou les tests système 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") }) }) ``` ## Optimisation des Performances Bien que les Importmaps éliminent le besoin de bundling, quelques optimisations restent pertinentes pour les applications en production. La configuration du cache HTTP est primordiale : ```ruby # config/environments/production.rb config.public_file_server.headers = { "Cache-Control" => "public, max-age=31536000, immutable" } ``` Le préchargement des modules critiques améliore le temps de chargement initial : ```erb <%# app/views/layouts/application.html.erb %> <%= javascript_importmap_tags %> <%= preload_link_tag "application", as: "script", type: "module" %> ``` Pour les modules externes volumineux, le chargement différé est recommandé : ```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) { // Configuration du graphique } } ``` ## Conclusion Rails Stimulus et Importmaps représentent une évolution significative dans la manière de construire des interfaces web interactives. En éliminant la complexité des outils de build traditionnels, cette approche permet aux développeurs de se concentrer sur la création de fonctionnalités plutôt que sur la configuration de l'environnement de développement. La combinaison de Stimulus pour le comportement JavaScript et de Turbo pour les mises à jour dynamiques offre une alternative puissante aux frameworks JavaScript lourds. Cette architecture respecte les principes fondamentaux de Rails : convention plutôt que configuration, et amélioration progressive du HTML. Pour les équipes qui recherchent une solution maintenable et performante sans la complexité des bundlers modernes, Stimulus et Importmaps constituent un choix pertinent en 2026. La courbe d'apprentissage réduite et l'intégration native avec Rails en font une option particulièrement attractive pour les projets de toute taille. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/ruby-on-rails/rails-stimulus-importmaps-modern-javascript-without-build-tools