BitaKit
Learn step by step

4. Add an Action

Define one operation as an Action and bind it to the Counter; every bound control shares its state.

Continue with the Counter service. An Action is BitaKit's central UI action and state management. Define an operation once; every control that triggers it, a button, menu item, link, search result or shortcut, shares its current label, availability rules, confirmation and execution state.

Read more about Actions →

1. Define the operation

Create src/plugins/counter/actions/increment.ts:

import { defineAction } from '@bitakit/core';

export default defineAction({
  title: 'Increment',
  execute: ({ services }) => services.counter.value.increment(),
});

The file names the Action counter.increment. It calls the service you already created.

2. Bind a button

Replace src/plugins/counter/ui/pages/index.tsx with:

'use client';
import { Action, useServiceState } from '@bitakit/ui';
import { Button, PageHeader } from '@bitakit/ui/components';

export default function CounterPage() {
  const count = useServiceState('counter.value', (state) => state.count, 0);
  return (
    <>
      <PageHeader title="Counter" />
      <output>{count}</output>
      <Action id="counter.increment">
        <Button />
      </Action>
    </>
  );
}

Click Increment. The number increases. The button gets its label and execution behavior from the Action; it needs no separate click handler.

You now have a complete feature: one page, one service and one Action.

3. Add one rule when needed

To cap this demonstration at 10, add enabled to the same Action:

export default defineAction({
  title: 'Increment',
  enabled: ({ services }) => services.counter.value.state.get().count < 10,
  execute: ({ services }) => services.counter.value.increment(),
});

Keep the existing import. Every binding updates as the store changes; at 10 the button is disabled. This optional example is a UI/execution rule, not server authorization.

The Action loop

The same loop works for a delete button, menu item or shortcut. In this example, removing a todo changes the service store; the Action's enabled function is re-evaluated and every bound control reads the new state.

One Action, every control

One Action, every control. The numbered path is one execution; the accent path is the state update that reaches every bound control. Guards and interceptors still apply.
The numbered path is one execution; the accent path is the state update that reaches every bound control. Guards and interceptors still apply.

Choose where an Action appears

With Shell installed, use slots: ['shell.navigation'] for navigation or slots: ['shell.toolbar'] for standard controls in the header toolbar. Shell renders the bound button or link for you. Component regions instead render a widget that binds the same Action. Select a destination below: the highlighted link shows the result, and the code shows exactly which file to create under src/plugins/.

App sidebar

Your plugin page

The owner renders this content. Moving the sample Action does not change the page.

The built-in toolbar sits beside custom header-end content in the same row. For a separate full-width row below the header, use shell.header.rows. On narrow screens, scroll the header illustration sideways to see all regions.

Navigation

Place the URL Action directly. Shell renders its current title and href as a native navigation link.

shell.navigation · actions

example/actions/open.ts
import { defineAction } from '@bitakit/core';export default defineAction({  title: 'Your action',  href: '/example',  slots: ['shell.navigation'],});
Choose a destination: the highlighted sample Action moves and the authoring example changes. This is an illustrative preview; it does not navigate or mutate your app.

Header start/end follow reading direction. The sidebar footer is available in the sidebar layout; it is separate from the app footer. An attached contribution only appears where its owner mounts an outlet. See Layouts and regions for the full map.

For an operation instead of a URL link, use execute in the Action and ActionButton inside the widget. Keep execution in the Action. To offer a destination in your own component, define and render a slot.

What you have now

  • This step gave you one operation, counter.increment. Every control bound to it gets the same label, disabled and pending state, and the same guards.
  • Others extend it by binding the same Action from their own UI with <Action id="counter.increment">, or by placing it in a slot.

Next: Save a preference →

Later: choose the option you need

NeedRead
Pass a value into an ActionInput
Navigate to a pageNavigation
Ask before a destructive operationConfirmation
Handle pending work or concurrent executionConcurrency
Bind a menu, toggle or custom controlBindings
Look up every propertyAction reference

Set these options only when the operation needs them. They are not part of the minimal Action definition.

On this page