slack-thunder
TypeScript icon, indicating that this package has built-in type declarations

0.1.1 • Public • Published

slack-thunder

Developing complex applications with @slack/bolt can be challenging using the standard approach (Block Kit). You may encounter the following issues:

  • Difficulty in composing interfaces from reusable parts
  • Inconvenient form management
  • Backend-centric design

Many agree that a composable component architecture is convenient for building UIs. This is where slack-thunder comes in. slack-thunder is a library that helps you build Block Kit interfaces using JSX. It employs familiar concepts like reusable components and hooks so you already kinda know how to use it. slack-thunder can be incrementally integrated into existing projects.

You can view the example application here.

Getting Started

Install using your preferred package manager:

yarn add slack-thunder @slack/bolt

In addition, you need to configure your transpiler to use the jsx-runtime from slack-thunder. If you're using tsc, add the following lines to your tsconfig.json file:

"jsx": "react-jsx",
"jsxImportSource": "slack-thunder"

For babel, set the importSource option of @babel/plugin-transform-react-jsx to slack-thunder. Refer to the importSource Documentation for more details.

Don't hesitate to open an issue if you encounter difficulties with integration.

Initialization

import { App } from '@slack/bolt'
import { AppComponent, render } from 'slack-thunder'

// Create @slack/bolt App
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
})

void (async () => {
  // Render AppComponent
  await render(<AppComponent slackApp={app} />)

  // Start @slack/bolt App
  await app.start(process.env.PORT || 3000)
  console.log('⚡️ Bolt app is running!')
})()

Handler Components

The fundamental building block of slack-thunder is a component. A component is a function (which can be async) that takes a props object and maps it to JSX UI.

const HelloMessage: Component<{ name: string }> = async ({ name }) => {
  await doSomeAsyncStuff()
  return (
    <Message token="your_slack_token" channel="C123ABC456">
      Hello, {name}
    </Message>
  )
}

A crucial special case of a component is a handler component. This entity handles user actions in Slack and produces a result. For instance, you might want to display a modal in response to a user's button click. That's when a handler component is useful.

const FancyModal: Component = () => {
  return (
    <Modal title="Fancy Modal">
      <section>
        <mrkdwn>*Some md text*</mrkdwn>
      </section>
      <divider />
      <actions>
        <button action_id="some_action_id">Fancy Button</button>
      </actions>
    </Modal>
  )
}

slack-thunder uses HandlerConfig objects to connect handler components and Slack events. This object describes which events a component should react to.

export const config: HandlersConfig = {
  actions: [{ pattern: 'SomeBlockKitAction', component: FancyModal }],
}

To allow slack-thunder to find your HandlerConfig objects, place them inside one of the modules in the src/app folder (you can specify a different path using the handlersPath prop of AppComponent).

.
└── src
    └── app
        ├── FancyModal.tsx
        └── components
            └── index.ts

[!IMPORTANT] Please note, all entries (direct children - files and folders) in src/app must export a config. If you use folder entries (to group related functionality), each entry must have an index file with a config export.

HandlerConfig is structured as follows:

type HandlersConfig = {
  actions?: (HandlerComponent | ConfigEntry)[]
  messages?: (HandlerComponent | ConfigEntry)[]
  shortcuts?: (HandlerComponent | ConfigEntry)[]
  commands?: (HandlerComponent | ConfigEntry)[]
  options?: (HandlerComponent | ConfigEntry)[]
  submissions?: (HandlerComponent | ConfigSubmissionEntry)[]
  events?: ConfigEventEntry[]
}

type ConfigEntry = { pattern?: string | RegExp; component: HandlerComponent }
type ConfigSubmissionEntry = ConfigEntry & { handleClose?: boolean }
type ConfigEventEntry = { pattern: string; component: HandlerComponent }

As you may notice, all entries (except events) can simply be HandlerComponent. This is because handler ids (patterns) for HandlerComponent-s are automatically generated. The example below illustrates this:

// path: src/app/hello.tsx

const HelloMessage: Component = () => (
  <Message channel="C123ABC456">
    Hello world!
  </Message>
)

const HelloModal: Component = () => {
  return (
    <Modal title="Hello Modal">
      <actions>
        <button action_id={HelloMessage}>Hello Button</button>
      </actions>
    </Modal>
  )

export const config: HandlersConfig = {
  actions: [HelloMessage, HelloModal]
}

In this example, HelloMessage and HelloModal components will automatically receive hello/HelloMessage and hello/HelloModal action ids, respectively. This allows us to use the HelloMessage component itself as a value for the HelloModal's action_id prop.

Handler components can have specific properties. For instance:

  • The options handler component has a query: string prop, which represents the current search query.
  • The submissions handler component has an eventType: 'view_submission' | 'view_closed' prop, which helps distinguish between submission and closed events.
  • All handler components have a rerender prop, useful for rerendering a component after an asynchronous operation. This can be utilized to display a loading state while data is being loaded, and then rerender the component once the data is available.

You can use the WithHandlerProps type to utilize these properties.

import type { WithHandlerProps } from 'slack-thunder'

type FormSubmissionProps = WithHandlerProps<
  { data?: Data },
  'eventType' | 'rerender'
>

const FormSubmission: Component<FormSubmissionProps> = ({
  data,
  eventType,
  rerender,
}) => {
  if (eventType === 'view_closed') {
    // Do something special on close
  }
  if (!data) {
    loadData().then(data => rerender({ data }))
    return // some layout in case when data is not available
  }
  // here data is available and you can return full layout
  return // some layout
}

Components

Message

The Message component is used to send, edit, or delete messages (either text or Block Kit) in channels.

Prop Description
token?: string Slack Web API Token. If you use the Message component outside of the handler (for example, in a background job), you must provide your token.
channel?: string Channel to send message to. However, if you use Message in response to an event that has a response_url (Message Responses Doc), you can omit this property.
messageTs?: string You can use the message timestamp to edit an existing message.
responseType?: 'in_channel' | 'ephemeral' | 'replace' | 'delete' Response type (Message Responses Doc).
onSuccess?: (data: ChatUpdateResponse | ChatPostMessageResponse) => void Callback to run on success.
onFail?: (error: Error) => void Callback to run on fail.
children?: ThunderNode String or any acceptable Block Kit blocks.

Modal

The Modal component is utilized to display a modal.

Prop Description
title: ThunderElement string
close?: ThunderElement string
submit?: ThunderElement string
callbackId?: Component string
notifyOnClose?: boolean Indicates whether Slack will send your request URL a view_closed event when a user clicks the close button. Defaults to false.
clearOnClose?: boolean When set to true, clicking on the close button will clear all views in a modal and close it. Defaults to false.
externalId?: string A custom identifier that must be unique for all views on a per-team basis.
privateMetadata?: object An optional object that will be sent to your app in view_submission and block_actions events.
children?: ThunderNode Any acceptable Block Kit blocks.

Home

The Home component is utilized to establish the layout of the user's Home Tab. For more information, refer to the Home Doc.

Props Description
token?: string Slack Web API Token. If you use the Home component outside of the handler (for example, in a background job), you must provide your token.
userId?: string ID of the user for whom you are setting the home tab.
externalId?: string A custom identifier that must be unique for all views on a per-team basis.
privateMetadata?: object An optional object that will be sent to your app in view_submission and block_actions events.
children?: ThunderNode Any acceptable Block Kit blocks.

Options and OptionGroups

These special components are used to display a dynamic list of options from an external data source. They can only be used in options field within the HandlersConfig.

const ProjectOptions: Component<WithHandlerProps<object, 'query'>> = async ({
  query,
}) => {
  const projects = await loadProjects(query)
  return (
    <Options>
      <option value="empty">No project</option>
      <option value="create_new">Create new</option>
      {projects.map(({ id, name }) => (
        <option value={id}>{name}</option>
      ))}
    </Options>
  )
}

export const config: HandlersConfig = {
  options: [ProjectOptions],
}
Props Descriptions
children: ThunderNode option elements for Options component or option_group for OptionGroups.

Hooks

useContext

At times, it's convenient to pass a value to a deeply nested component without resorting to "props drilling". For instance, you might have an i18n function that you want to use in nearly all components.

[!IMPORTANT] Due to the way it's implemented internally, you must use the useContext hook before any asynchronous action in your component.

// Create context
const MyContext = createContext<{ myValue: unknown } | undefined>(undefined)

// Provide a value
<MyContext.Provider value={{ myValue: someValue }}>
  ...
</MyContext.Provider>

// Using context value
const myContext = useContext(MyContext)

useArgs

In HandlerComponent or its child components, you can use the useArgs hook to retrieve the current handler’s arguments object (Listener function arguments).

usePrivateMetadata

Use this hook to access the current view's private_metadata. You can use private_metadata to transfer information between views. For more information, see Carry data between views.

const metadata = usePrivateMetadata<MetadataType>()

useState

This is a particularly interesting hook that allows you to work with forms in a convenient and type-safe way. It's best illustrated by an example:

const Form: Component = () => {
  const [current, stateRef] = useState(state)
  console.log(current) // Log current state

  return (
    <Modal title="Form" callbackId={FormSubmission}>
      <input block_id="projects" label="Choose project" dispatch_action>
        <external_select action_id={stateRef.projects} />
      </input>

      <input block_id="name" label="Name" dispatch_action>
        <plain_text_input action_id={stateRef.name} />
      </input>

      <input block_id="someSelect" label="Some select" dispatch_action>
        <static_select
          action_id={stateRef.someSelect}
          options={
            <>
              <option value="1">Option 1</option>
              <option value="2">Option 2</option>
            </>
          }
        />
      </input>
    </Modal>
  )
}

const FormSubmission: Component = () => {
  const [current] = useState(state)
  // Do something with the state
}

const state = createState(Form, {
  projects: { type: 'external_select', options: ProjectOptions },
  name: 'plain_text_input',
  someSelect: 'static_select',
})

export const config: HandlersConfig = {
  actions: [Form],
  submissions: [FormSubmission],
  options: [ProjectOptions],
}

Intrinsic Elements

Refer to jsx-types.ts to view the available elements, their props, and possible values. This file also contains comments that clarify the context in which a particular element can be used.

/**
 * Blocks: Section, Actions
 * Surfaces: Message
 */
workflow_button: {
  /** text: plain_text */
  children: ThunderNode

In this example, the workflow_button element can be used in Messages and can contain Section and Actions blocks. Its children prop is an alias for the text field and can only be a plain_text element.

Package Sidebar

Install

npm i slack-thunder

Weekly Downloads

6

Version

0.1.1

License

none

Unpacked Size

110 kB

Total Files

18

Last publish

Collaborators

  • schusovskoi