Unleash the Power of Hotwire(Part 2): Getting Started with Stimulus
by Satya Swaroop Mohapatra, Senior System Analyst

In Part 1, we explored how Turbo Drive, Turbo Frames, and Turbo Streams help us create seamless server-side updates. Now, let's learn about Stimulus - a simple yet powerful JavaScript framework that helps us add client-side interactivity when we need it.
In this part, we'll start with the basics and build our understanding through simple examples:
- Understanding Stimulus Controllers and Lifecycle Methods
- Working with Targets to interact with DOM elements
- Handling Events with Actions
- Using Values to manage and sync state
Getting Started with Stimulus
Let's start by creating a simple counter example to understand the basics. First, generate a Stimulus controller:
rails g stimulus counter
This creates a new file app/javascript/controllers/counter_controller.js
:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
console.log("Counter controller connected!")
}
}
Understanding Lifecycle Methods
Stimulus controllers have three main lifecycle methods that help us manage our code:
export default class extends Controller {
initialize() {
// Called once when the controller is first instantiated
console.log("Counter initialized!")
}
connect() {
// Called when the controller is connected to the DOM
console.log("Counter connected!")
}
disconnect() {
// Called when the controller is disconnected from the DOM
console.log("Counter disconnected!")
}
}
Let's create a simple view to use our counter:
<div data-controller="counter">
<h1>Simple Counter</h1>
<p>Open your browser console to see the lifecycle methods in action!</p>
</div>
When you load this page, you'll see the lifecycle messages in your console. This helps us understand when our controller is active.
Working with Targets
Now let's make our counter functional. We'll need to reference some DOM elements - this is where targets come in. They're like querySelector but much simpler to use.
Update the counter controller:
export default class extends Controller {
static targets = ["output"]
connect() {
this.count = 0
this.updateOutput()
}
increment() {
this.count++
this.updateOutput()
}
updateOutput() {
this.outputTarget.textContent = this.count
}
}
And update our view:
<div data-controller="counter">
<h1>Simple Counter</h1>
<p>Count: <span data-counter-target="output">0</span></p>
<button data-action="click->counter#increment">Increment</button>
</div>
Let's break down what's happening:
- We define targets using
static targets = ["output"]
- We reference a target using
this.outputTarget
- The view connects to targets using
data-counter-target="output"
Handling Events with Actions
You might have noticed data-action="click->counter#increment"
in our button. This is how Stimulus handles events:
click
is the event typecounter
is our controller nameincrement
is the method to call
Let's add a few more actions to make our counter more interesting:
export default class extends Controller {
static targets = ["output"]
connect() {
this.count = 0
this.updateOutput()
}
increment() {
this.count++
this.updateOutput()
}
decrement() {
this.count--
this.updateOutput()
}
reset() {
this.count = 0
this.updateOutput()
}
updateOutput() {
this.outputTarget.textContent = this.count
}
}
And update our view:
<div data-controller="counter" class="p-4">
<h1 class="text-xl mb-4">Simple Counter</h1>
<p class="mb-4">Count: <span data-counter-target="output" class="font-bold">0</span></p>
<div class="space-x-2">
<button data-action="click->counter#decrement"
class="px-4 py-2 bg-red-500 text-white rounded">
Decrease
</button>
<button data-action="click->counter#increment"
class="px-4 py-2 bg-green-500 text-white rounded">
Increase
</button>
<button data-action="click->counter#reset"
class="px-4 py-2 bg-gray-500 text-white rounded">
Reset
</button>
</div>
</div>
Understanding Stimulus Values
In our counter example, we're storing the count in a controller property (this.count
). While this works, Stimulus provides a better way to handle state: Values
. Values let us declare our controller's state and automatically sync it with data attributes in the DOM.
Let's improve our counter using Values:
export default class extends Controller {
static targets = ["output"]
static values = {
count: { type: Number, default: 0 }
}
connect() {
this.updateOutput()
}
increment() {
this.countValue++
this.updateOutput()
}
decrement() {
this.countValue--
this.updateOutput()
}
reset() {
this.countValue = 0
this.updateOutput()
}
updateOutput() {
this.outputTarget.textContent = this.countValue
}
}
And our updated view:
<div data-controller="counter"
data-counter-count-value="0"
class="p-4">
<h1 class="text-xl mb-4">Simple counter</h1>
<p class="mb-4">Count: <span data-counter-target="output" class="font-bold">0</span></p>
<div class="space-x-2">
<button data-action="click->counter#decrement"
class="px-4 py-2 bg-red-500 text-white rounded">
Decrease
</button>
<button data-action="click->counter#increment"
class="px-4 py-2 bg-green-500 text-white rounded">
Increase
</button>
<button data-action="click->counter#reset"
class="px-4 py-2 bg-gray-500 text-white rounded">
Reset
</button>
</div>
</div>
Benefits of using Values:
- State is declared explicitly in the controller
- Changes are reflected automatically in the DOM
- Type checking is built-in
- Values can be observed via change callbacks
Key Takeaways
Through these examples, we've learned the core concepts of Stimulus:
- Controllers and Lifecycle Methods - How controllers connect to the DOM
- Targets - How to reference DOM elements
- Actions - How to handle user interactions
- Values - How to manage and sync state
Remember, Stimulus is designed to augment your HTML, not take it over. By understanding these concepts, you can add just the right amount of JavaScript behavior to your Rails applications while keeping them maintainable and scalable.