Components

Views and Buttons: Persistent Views, Dynamic Custom IDs, Permission Checks, and Routing

Build button systems for tickets, moderation, verification, pagination, and confirmations.

discord.py 2.xComponentsPythonmodern-discord-api

1. What Is It?

A View is a Python object that owns Discord UI items and their callbacks.

In human terms: a Discord bot is a small service waiting for Discord to say, "someone did something." Your job is to understand that signal, make a safe decision, and send back a response that feels instant and clear.

2. Why Does It Exist?

Large bots use views to keep interaction UI close to callback code while delegating real work to services.

This exists because real communities need predictable interfaces. Buttons, menus, modals, slash commands, events, databases, and services each remove a different kind of confusion from the user and the developer.

3. How Discord Handles It Internally

Discord sends a component interaction; discord.py dispatches it to the matching item callback when the view is registered.

The important production rule is timing: acknowledge interactions quickly, defer when work may take longer, and keep secrets, permissions, and persistent state outside the visible component payload.

4. Visual Explanation

Views and Buttons: Persistent Views, Dynamic Custom IDs, Permission Checks, and Routing flow
LayerWhat happensProduction question
Discord layerReceives Button Click and sends an async interaction or gateway event.Which intent, permission, or response deadline applies?
Python layerThe event loop dispatches View Dispatch without blocking other callbacks.Are you awaiting I/O instead of blocking the loop?
Service layerRuns Service with persistence, logging, and clear errors.What survives restart and what must be re-registered?

5. Beginner Example

This beginner example intentionally keeps the moving parts visible.

import discord
 
class ContinueView(discord.ui.View):
    @discord.ui.button(label="Continue", style=discord.ButtonStyle.primary, custom_id="academy:continue")
    async def continue_button(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_message("Choose the next safe action.", ephemeral=True)

Line-by-line mindset: identify the user action, create the smallest valid response, and keep state either in Discord's interaction payload or in a service you control.

6. Intermediate Example

The intermediate version separates routing from the feature logic.

from dataclasses import dataclass
 
@dataclass(frozen=True)
class Route:
    namespace: str
    action: str
    state: str = "default"
 
def parse_route(custom_id: str) -> Route:
    namespace, action, *rest = custom_id.split(":")
    return Route(namespace=namespace, action=action, state=rest[0] if rest else "default")
 
async def route_interaction(custom_id: str) -> dict[str, str | bool]:
    route = parse_route(custom_id)
    if route.namespace != "academy":
        return {"ok": False, "reason": "Unknown component namespace"}
    return {"ok": True, "action": route.action, "state": route.state}

This is the point where beginners become maintainers: the code is still small, but each file has a reason to exist.

7. Production Example

Production code adds validation, persistence, logging, and recovery boundaries.

import time
import discord
 
async def handle_production_action(interaction: discord.Interaction) -> None:
    started_at = time.perf_counter()
 
    if interaction.guild is None:
        await interaction.response.send_message("Use this inside a server.", ephemeral=True)
        return
 
    permissions = interaction.user.guild_permissions if isinstance(interaction.user, discord.Member) else None
    if permissions is None or not permissions.manage_guild:
        await interaction.response.send_message("You do not have permission for this action.", ephemeral=True)
        return
 
    await interaction.response.defer(ephemeral=True)
    await audit_log.write(
        guild_id=interaction.guild.id,
        user_id=interaction.user.id,
        custom_id=interaction.data.get("custom_id") if interaction.data else "unknown",
        latency_ms=round((time.perf_counter() - started_at) * 1000),
    )
    await interaction.followup.send("Action completed and recorded safely.", ephemeral=True)

Large bots use this shape because one slow database call, one missing permission check, or one reused custom ID can break thousands of interactions.

Function-by-function walkthrough

This table explains the functions, classes, methods, and helpers that appear throughout the examples. Read it like a map: the name tells you what you call, the middle column explains what it does, and the last column explains how production bots should think about it.

8. Common Mistakes

  • Forgetting that component custom IDs are user-controlled routing inputs.
  • Treating ephemeral responses as private storage instead of temporary user-facing messages.
  • Doing slow database or API work before acknowledging an interaction.
  • Copying old snippets without checking library version and Discord API behavior.

9. Best Practices

  • Name custom IDs with a namespace, action, and short state key.
  • Keep handlers thin; put business decisions in services.
  • Register commands and persistent UI intentionally during startup or deployment.
  • Prefer explicit permission checks over assumptions based on channel visibility.
  • Keep examples readable, then show the production boundary separately.

10. Performance Notes

Cache data that is safe to cache, but never confuse cache with source of truth. Use deferred responses for slow work, batch database reads when possible, and avoid running heavy computation inside the interaction callback itself.

11. Security Notes

Never store tokens in Git. Validate user, guild, channel, role, and message state before taking action. For component systems, assume a user can click an old message after a deploy and design the handler to re-check current permissions.

12. Challenge Section

  1. Draw the lifecycle for this topic from user action to final response.
  2. Add one permission guard and one logging statement to the beginner example.
  3. Convert the intermediate example into a service with tests or validation.
  4. Explain what would break if this feature were used by 10,000 guilds.

Focus on the user story: what the person clicks, selects, submits, or expects from Views and Buttons: Persistent Views, Dynamic Custom IDs, Permission Checks, and Routing.