Create your own
Lesson illustration

Building a Back Button for Parent Menus

Welcome back! In our previous lesson, we successfully built a basic menu router from first principles. By creating a structured callback_data format and using a regular expression handler, we were able to edit a message to navigate between a main menu and a settings sub-menu. Our "back" button was simply a link with the data "menu:main".

Today, we'll build on that foundation by implementing a more robust and scalable "back" button functionality. To do this, we'll refactor our manual implementation to use grammY's official menu plugin. This powerful abstraction handles the complexities of menu navigation, including parent-child relationships, allowing us to build complex UIs declaratively. The goal is to implement a 'back' button that automatically navigates to a parent menu, leveraging the plugin's built-in capabilities.

Refactoring to the menu Plugin

From your experience with front-end frameworks, you know that while you can build a client-side router by manually parsing URL hashes, you almost always use a library like Vue Router or React Router. The grammY menu plugin serves the same purpose: it provides a declarative, feature-rich API for a common and complex task.

Let's refactor our previous example. Instead of using InlineKeyboard, we'll now use the Menu class from the @grammyjs/menu plugin. First, make sure you've installed it:

bun add @grammyjs/menu

Now, let's redefine our menus. The key difference is that each Menu instance requires a unique string identifier. This ID is how menus refer to each other.

import { Bot, Context } from "grammy";
import { Menu } from "@grammyjs/menu";
import "dotenv/config";

// --- Menu Definitions ---
const mainText = "Welcome! This is the main menu. Choose an option:";
const settingsText = "Here are the settings. What would you like to configure?";

// 1. Create the Settings Sub-Menu (child)
// We give it a unique ID: 'settings-menu'
const settingsMenu = new Menu("settings-menu")
  .text("🔔 Notifications", (ctx) => ctx.reply("Toggling notifications!"))
  .row() // Puts the next button on a new row
  .back("⬅️ Back"); // This is the magic!

// 2. Create the Main Menu (parent)
// Unique ID: 'main-menu'
const mainMenu = new Menu("main-menu")
  .text("📊 Status", (ctx) => ctx.reply("Status is OK!"))
  .submenu("⚙️ Settings", "settings-menu"); // Navigates to the 'settings-menu'

// 3. Register the sub-menu with its parent.
// This establishes the parent-child relationship.
mainMenu.register(settingsMenu);

Let's break down what's happening here:

  1. We define settingsMenu with the ID "settings-menu". Instead of a button with custom callback_data for going back, we simply call .back("⬅️ Back"). The plugin automatically understands that this should navigate to the parent menu.
  2. We define mainMenu with the ID "main-menu". To link to the settings menu, we use .submenu("⚙️ Settings", "settings-menu"). This method creates a button that, when pressed, navigates to the menu with the specified ID.
  3. Critically, we call mainMenu.register(settingsMenu). This tells the mainMenu that settingsMenu is one of its children. This is how the .back() button on the child knows its destination.

Integrating the Menu Plugin with the Bot

With our menu hierarchy defined, integrating it into the bot is incredibly simple. We just need to tell the bot to use the root menu instance as middleware.

// --- Bot Initialization ---
const token = process.env.BOT_TOKEN;
if (!token) throw new Error("BOT_TOKEN is not set in .env file");

const bot = new Bot(token);

// Use the menu plugin.
// This single line replaces our entire regex-based callback_query handler!
bot.use(mainMenu);

// --- Command Handlers ---
bot.command("start", async (ctx) => {
  // Send the menu.
  await ctx.reply(mainText, {
    reply_markup: mainMenu,
  });
});

// --- Start the Bot ---
bot.start();

console.log("Bot started!");

Notice that our entire bot.callbackQuery(/^menu:(.+)/, ...) handler is gone. The bot.use(mainMenu) middleware now handles all button clicks and navigation automatically. When a user clicks the "Settings" button, the plugin finds the registered "settings-menu" and edits the message. When they click "Back," the plugin knows to navigate back to "main-menu" because of the registered relationship.

To fully grasp how the plugin creates these connections, let's look at the official documentation.

MenuRange | grammY

The grammY documentation for the MenuRange class provides the formal definitions for the methods we just used. Understanding these will clarify how the plugin builds the navigation graph.

First, read the documentation for the .submenu() method. This explains how you create a button that navigates to another menu using its identifier and the importance of registering the target menu. Next, review the section for the .back() method. Note how it simply adds a button that navigates to the parent menu, abstracting away the need for you to manage the destination.

By using .submenu() and .back(), you've created a declarative and maintainable menu structure. Adding a new sub-menu, or even a sub-sub-menu, is now a matter of defining a new Menu instance, registering it with its parent, and adding a .submenu() button. The "back" navigation will work automatically at every level of the hierarchy.

Conclusion

In this lesson, you have refactored our manual menu system to use the official grammY menu plugin, resulting in code that is cleaner, more declarative, and far more scalable. You have learned how to create a true parent-child relationship between menus and how to implement a "back" button that leverages this relationship for automatic navigation.

Key Takeaways:

  • The grammY menu plugin abstracts away the manual handling of callback_data and message editing for navigation.
  • Menus are defined with unique string IDs using the Menu class constructor (e.g., new Menu("main-menu")).
  • The .submenu("Label", "child-menu-id") method creates a button that navigates to a child menu.
  • The .back("Label") method creates a button that automatically navigates to the parent menu.
  • A parent menu must register its children using .register() to establish the navigation hierarchy (e.g., mainMenu.register(settingsMenu)).
  • The entire menu system is activated by installing the root menu as middleware with bot.use(mainMenu).

With your understanding of menu structure and navigation firmly in place, you are well-equipped to design the complex interactive UIs you envisioned. In our next lesson, we will move to a new topic: Advanced Control Flow with Middleware. We'll explore how grammY's middleware system works, allowing you to intercept, process, and control the flow of updates for tasks like logging and authentication.

Can't find a good explanation? Sign up and we'll make it for you

Sign up