[BUG] Dispatch actions fail when modal has been updated with client.views.update
The deno-slack versions
"deno-slack-sdk/": "https://deno.land/x/[email protected]/",
"deno-slack-api/": "https://deno.land/x/[email protected]/"
Deno runtime version
deno 2.2.3 (stable, release, x86_64-pc-windows-msvc)
v8 13.4.114.11-rusty
typescript 5.7.3
OS info
Microsoft Windows [Version 10.0.19045.5487]
Describe the bug
When a modal is opened using client.views.open, the modal displays correctly. However, after the modal is updated via client.views.update to include interactive elements (with dispatch_action: true), these elements do not trigger any block action events when interacted with. Although the updated modal renders without errors, user interactions (e.g. selecting a user in a users select element) produce no event payloads, and Slack logs no detailed errors.
Steps to reproduce
- Create a basic Slack app using the deno-slack-sdk.
- Open a modal using
client.views.openand output the returned view ID and hash. - Update the modal view via
client.views.update, ensuring that interactive elements (withdispatch_action: true) are present in the updated view. - Interact with one of the interactive elements in the updated modal.
- Observe that no block action payload is received by the handler—the UI briefly shows a loading spinner, then displays an error icon—while no error messages or logs are produced by Slack.
Expected result
Interactive elements in the updated modal should trigger block action events, and the registered action handlers (via addBlockActionsHandler or addViewSubmissionHandler) should receive the interactive payload.
Actual result
After the modal is updated with client.views.update, interactive elements do not dispatch any events. The modal appears unresponsive to user interactions even though all interactive properties (e.g. dispatch_action: true) are present. No error messages or payload logs are received from Slack.
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
// Function to open a modal
export const FetchFunctionDefinition = DefineFunction({
callback_id: "fetch-modal",
title: "Fetch modal",
source_file: "functions/fetch_modal.ts",
input_parameters: {
properties: { interactivity: { type: Schema.slack.types.interactivity } },
required: ["interactivity"],
},
output_parameters: {
properties: {
view_id: { type: Schema.types.string },
hash: { type: Schema.types.string },
},
required: ["view_id", "hash"],
},
});
export default SlackFunction(
FetchFunctionDefinition,
async ({ inputs, client }) => {
const { interactivity: { interactivity_pointer: trigger_id } } = inputs;
const resp = await client.views.open({
trigger_id,
view: {
type: "modal",
callback_id: "fetch_modal",
title: { type: "plain_text", text: "Fetching information" },
blocks: [{
type: "section",
text: {
type: "plain_text",
text: "Fetching information. Please wait...",
},
}],
},
});
return {
outputs: { view_id: resp.view.id, hash: resp.view.hash },
};
},
);
// Function to update the modal with an interactive element that uses dispatch_action
export const UpdateFunctionDefinition = DefineFunction({
callback_id: "update-modal",
title: "Update modal",
source_file: "functions/update_modal.ts",
input_parameters: {
properties: {
view_id: { type: Schema.types.string },
hash: { type: Schema.types.string },
},
required: ["view_id", "hash"],
},
});
export default SlackFunction(
UpdateFunctionDefinition,
async ({ inputs, client }) => {
const { view_id: viewId, hash } = inputs;
await client.views.update({
view_id: viewId,
hash: hash,
view: {
type: "modal",
callback_id: "update_modal",
submit: { type: "plain_text", text: "Update" },
close: { type: "plain_text", text: "Close" },
title: { type: "plain_text", text: "Update" },
blocks: [
{
type: "section",
block_id: "role_block",
text: { type: "mrkdwn", text: "Select a user:" },
accessory: {
type: "users_select",
action_id: "role_action",
dispatch_action: true,
},
},
],
},
});
return { completed: false };
},
)
.addBlockActionsHandler(["role_action"], async ({ action }) => {
console.log("Incoming action handler invocation", action);
});
For context, the intended workflow is to use an initial modal step to capture an interaction payload and open a placeholder modal to avoid timing out. In a subsequent step, values are fetched from a datastore (or similar source) and then used to update the modal view with pre-filled input values. Up to that point, everything works fine and the view is updated as expected. However, once the modal is updated via client.views.update, any interaction with the updated input elements (e.g., user selects) fails to trigger any response.
I reviewed the documentation and couldn’t find any specific notes on this behavior. I suspect that updating the view on its own—without a fresh user interaction—might be causing Slack to “use up” the initial interactivity payload, leading to subsequent dispatch actions not being recognized.
Hi @Aze331 - thanks for bringing this to our attention! The team's been out this week for a conference, so give us some time to look into reproducing this and we'll get back to you with a proper response. 🙂
@Aze331 thanks for writing in 💯
This seems to be expected behavior 🤔 the Handling and responding to interactions of our modals docs, mention the following
When someone uses an interactive component in your app's views, the app receives a block_actions payload. This does not apply to components included in an input block.
Selecting a user in a select user element is a subset of the input block. To work around this you can use available action buttons with views.update to create a multi steps model experience, this should allow you to display specific inputs based on previously submitted values
@Aze331 thanks for writing in 💯
This seems to be expected behavior 🤔 the Handling and responding to interactions of our modals docs, mention the following
When someone uses an interactive component in your app's views, the app receives a block_actions payload. This does not apply to components included in an input block.
Selecting a user in a
select user elementis a subset of the input block. To work around this you can use available action buttons withviews.updateto create a multi steps model experience, this should allow you to display specific inputs based on previously submitted values
Thank you for the update @misscoded! And thank you @WilliamBergamin for taking a stab at my issue!
This does not apply to components included in an input block (see below for details about those).
This confuses me even more, because I do get block_actions payloads when I make changes to the input directly after the view has been opened. I can even update the view and continue changing inputs and get block_actions payloads.
See the quick and dirty example below
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
export const FetchFunctionDefinition = DefineFunction({
callback_id: "fetch-modal",
title: "Fetch modal",
source_file: "functions/fetch_modal.ts",
input_parameters: {
properties: { interactivity: { type: Schema.slack.types.interactivity } },
required: ["interactivity"],
},
});
export default SlackFunction(
FetchFunctionDefinition,
async ({ inputs, client }) => {
const { interactivity: { interactivity_pointer: trigger_id } } = inputs;
const resp = await client.views.open({
trigger_id,
view: {
type: "modal",
callback_id: "update_modal",
submit: { type: "plain_text", text: "Update" },
close: { type: "plain_text", text: "Close" },
title: { type: "plain_text", text: "Update" },
blocks: [
{
type: "section",
block_id: "role_block",
text: { type: "mrkdwn", text: "role" },
accessory: {
type: "users_select",
action_id: "role_action",
},
},
],
},
});
return { completed: false };
},
)
.addBlockActionsHandler(["role_action"], async ({ action }) => {
console.log("Incoming action handler invocation", action);
});
But when I separate the steps of views.open and views.update into two separate custom functions it does not give me any block_actions payloads
Maybe I'm misinterpreting something? I'm just confused because it works fine as long as I have the views.update in the same custom function as where i do views.open. But I want to separate these two functions because then I could use inputs and wouldn't have to juggle variables in the metadata for example.