Quick Start¶
This guide walks through building a simple stateful counter to introduce the core concepts.
The Counter Example¶
A counter with increment and decrement buttons, backed by the state store:
import discord
from discord.ext import commands
from discord.ui import ActionRow, TextDisplay
from cascadeui import (
StatefulLayoutView, StatefulButton, cascade_reducer,
card, key_value, divider,
)
# 1. Define a reducer for your custom action
@cascade_reducer("COUNTER_UPDATED")
async def counter_reducer(action, state):
# @cascade_reducer passes a deep copy — mutate and return directly
state.setdefault("application", {}).setdefault("counters", {})
view_id = action["payload"]["view_id"]
state["application"]["counters"][view_id] = action["payload"]["counter"]
return state
# 2. Create a stateful view
class CounterView(StatefulLayoutView):
session_limit = 1
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.counter = 0
self._build_ui()
def _build_ui(self):
self.clear_items()
self.add_item(
card(
"## Counter",
key_value({"Value": str(self.counter)}),
color=discord.Color.blurple(),
)
)
self.add_item(ActionRow(
StatefulButton(label="+1", style=discord.ButtonStyle.primary,
callback=self.increment),
StatefulButton(label="-1", style=discord.ButtonStyle.danger,
callback=self.decrement),
))
self.add_exit_button()
async def _update(self, interaction):
await interaction.response.defer()
await self.dispatch("COUNTER_UPDATED", {
"view_id": self.id,
"counter": self.counter,
})
self._build_ui()
if self.message:
await self.message.edit(view=self)
async def increment(self, interaction):
self.counter += 1
await self._update(interaction)
async def decrement(self, interaction):
self.counter -= 1
await self._update(interaction)
# 3. Wire it up to a command
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
@bot.hybrid_command()
async def counter(ctx):
view = CounterView(context=ctx)
await view.send()
import discord
from discord.ext import commands
from cascadeui import StatefulView, StatefulButton, cascade_reducer
# 1. Define a reducer for your custom action
@cascade_reducer("COUNTER_UPDATED")
async def counter_reducer(action, state):
# @cascade_reducer passes a deep copy — mutate and return directly
state.setdefault("application", {}).setdefault("counters", {})
view_id = action["payload"]["view_id"]
state["application"]["counters"][view_id] = action["payload"]["counter"]
return state
# 2. Create a stateful view
class CounterView(StatefulView):
session_limit = 1
def __init__(self, context):
super().__init__(context=context)
self.counter = 0
self.add_item(StatefulButton(
label="+1",
style=discord.ButtonStyle.primary,
callback=self.increment,
))
self.add_item(StatefulButton(
label="-1",
style=discord.ButtonStyle.danger,
callback=self.decrement,
))
self.add_exit_button()
async def increment(self, interaction):
await interaction.response.defer()
self.counter += 1
await self.dispatch("COUNTER_UPDATED", {
"view_id": self.id,
"counter": self.counter,
})
if self.message:
await self.message.edit(
embed=discord.Embed(title="Counter", description=f"Value: {self.counter}"),
view=self,
)
async def decrement(self, interaction):
await interaction.response.defer()
self.counter -= 1
await self.dispatch("COUNTER_UPDATED", {
"view_id": self.id,
"counter": self.counter,
})
if self.message:
await self.message.edit(
embed=discord.Embed(title="Counter", description=f"Value: {self.counter}"),
view=self,
)
# 3. Wire it up to a command
bot = commands.Bot(command_prefix="!", intents=discord.Intents.default())
@bot.hybrid_command()
async def counter(ctx):
view = CounterView(context=ctx)
await view.send(embed=discord.Embed(title="Counter", description="Value: 0"))
What Just Happened?¶
-
@cascade_reducerregistered a function that handlesCOUNTER_UPDATEDactions. Reducers receive the action and a deep-copied state, mutate it, and return it directly — nocopy.deepcopy()needed. -
StatefulLayoutView(V2) orStatefulView(V1) wraps discord.py's view classes with state integration. They auto-subscribe to the state store and handle cleanup on timeout. -
StatefulButtonextends discord.py'sButtonwith automaticCOMPONENT_INTERACTIONdispatching. Every click is tracked in the state store. -
view.dispatch()sends an action through the middleware pipeline into the reducer, which updates state. Subscribers (including the view itself) are notified. -
view.send()handles message creation, state registration, and message tracking in one call. V2 views send the component tree as the message content. V1 views acceptembedandcontentparameters.
Next Steps¶
- State Management — understand the dispatch/reducer cycle
- Views — lifecycle, transitions, and pre-built patterns
- Components — buttons, selects, V2 helpers, wrappers, and composition
- Persistence — save and restore state across restarts
- Known Limitations — architectural constraints to be aware of