Skip to main content

Menu Context

Every callback in FlowCord — setup, setEmbeds, setButtons, onEnter, action handlers, and all other hooks — receives a context object (ctx). It is the single interface through which your code interacts with the running session.

Full interface​

interface MenuContext<TState, TSessionState, TOptions> {
// State
state: StateAccessor<TState>;
sessionState: StateStore<TSessionState>;

// Navigation
goTo(menuId: string, options?: Record<string, unknown>): Promise<void>;
goBack(result?: unknown): Promise<void>;
close(): Promise<void>;
hardRefresh(): Promise<void>;
openSubMenu(menuId: string, opts: SubMenuOptions): Promise<void>;
complete(result?: unknown): Promise<void>;

// Discord
client: Client<true>;
interaction: Interaction;

// Menu info
menu: MenuInstanceLike;
session: MenuSessionLike;
options: TOptions;
pagination: PaginationState | null;
}

State​

ctx.state — StateAccessor<TState>​

Menu-local state, scoped to the current menu instance. Typed via the TState generic on MenuBuilder.

new MenuBuilder<{ count: number }>(session, 'counter')
.setup((ctx) => {
ctx.state.set('count', 0);
})
.setButtons((ctx) => [
{
label: `Clicked ${ctx.state.get('count')} times`,
action: async (ctx) => {
ctx.state.set('count', ctx.state.get('count') + 1);
},
},
])
// ...
.build();

State is reset when the menu is recreated (e.g. on goBack()), unless the menu opts into .setPreserveStateOnReturn(). See State Management for the full picture.

ctx.sessionState — StateStore<TSessionState>​

Session-wide state shared across all menus in the current session. Typed via the second generic on MenuBuilder.

new MenuBuilder<MyState, { userId: string }>(session, 'profile')
.setup((ctx) => {
const userId = ctx.sessionState.get('userId');
})

Unlike ctx.state, session state persists across navigation and is not reset when menus change. See State Management.

ctx.goTo(menuId, options?)​

Navigate forward to another registered menu.

action: async (ctx) => {
await ctx.goTo('settings', { tab: 'notifications' });
}

The options object is available in the target menu as ctx.options.

ctx.goBack(result?)​

Pop the navigation stack and return to the previous menu. If the stack is empty and a fallback menu is configured, navigates there instead. If the stack is empty and no fallback exists, closes the session.

Passing result is used in the sub-menu continuation pattern — the parent's onComplete callback receives it.

ctx.close()​

End the session immediately. The menu message has its components disabled.

ctx.hardRefresh()​

Re-run the menu factory from scratch before the next render. Use this when the menu's structure itself needs to change — for example, when the set of buttons depends on data that has changed externally. For simple state-driven UI changes, just mutate ctx.state and let the auto-render handle it.

ctx.openSubMenu(menuId, opts) / ctx.complete(result?)​

The sub-menu pattern — for parent menus that open a child menu and receive a result back. See Sub-Menus & Continuations.

Discord​

ctx.client — Client<true>​

The logged-in discord.js Client instance. Useful for fetching guild members, channels, or other Discord resources during action callbacks.

ctx.interaction — Interaction​

The most recent Discord interaction associated with this session. This updates on every user action. Useful if you need to inspect the raw interaction — though in most cases ctx.state and ctx.options are sufficient.

ctx.session — MenuSessionLike​

Exposes read-only information about the current session:

interface MenuSessionLike {
readonly id: string; // Unique session ID (12-char)
readonly sessionState: StateStore; // Same as ctx.sessionState
readonly isCancelled: boolean; // True if user pressed Cancel
readonly isCompleted: boolean; // True if session has ended
}

ctx.menu — MenuInstanceLike​

Exposes information about the current menu:

interface MenuInstanceLike {
readonly name: string; // Menu name as registered
readonly mode: 'embeds' | 'layout'; // Active render mode
}

ctx.options — TOptions​

The options object passed when this menu was opened — either from goTo(menuId, options) or from the third argument to flowcord.handleInteraction(interaction, menuId, options).

// Opening with options
await ctx.goTo('item-detail', { itemId: '42', readOnly: true });

// Receiving options in the target menu
new MenuBuilder<State, SessionState, { itemId: string; readOnly: boolean }>(session, 'item-detail')
.setup((ctx) => {
const { itemId, readOnly } = ctx.options;
})

At runtime ctx.options is always Record<string, unknown> — the generic TOptions gives you compile-time type safety, but no runtime validation is performed.

ctx.pagination — PaginationState | null​

Pagination state for the current render cycle. null when no pagination is configured. When active:

interface PaginationState {
currentPage: number; // 0-indexed
totalPages: number;
itemsPerPage: number;
totalItems: number;
startIndex: number; // Use with Array.slice(startIndex, endIndex)
endIndex: number; // Exclusive
}

Use startIndex and endIndex to slice your data array in setEmbeds or setLayout. See Pagination.

TypeScript generics​

MenuBuilder accepts four generic parameters:

MenuBuilder<
TState extends Record<string, unknown>, // ctx.state type
TSessionState extends Record<string, unknown>, // ctx.sessionState type
TCtx extends MenuContext<TState, TSessionState>, // ctx type (for builder subclasses)
TMode extends 'unset' | 'embeds' | 'layout' // render mode (enforced at compile time)
>

In practice you'll usually only specify the first two:

type MyState = { count: number; label: string };
type MySessionState = { userId: string };

new MenuBuilder<MyState, MySessionState>(session, 'my-menu')
.setup((ctx) => {
// ctx.state.get('count') → number
// ctx.sessionState.get('userId') → string
})

TCtx and TMode are inferred automatically unless you're building a custom MenuBuilder subclass.