Actions-based Dispatching
Action-based dispatching provides a more intuitive and function-like approach to dispatching actions in Context-Action. Instead of using the traditional registry.dispatch() method, you can call actions directly as functions through the registry.actions property.
Overview
The actions property provides a function-based interface where each registered action becomes a callable function. This approach offers better developer experience with improved type safety and more intuitive syntax.
// Traditional approach
await registry.dispatch('userLogin', { userId: '123', email: 'user@example.com' });
// Actions-based approach
await registry.actions.userLogin({ userId: '123', email: 'user@example.com' });<!-- @context-action-compile -->
import { ActionRegister, type ActionPayloadMap } from '@context-action/core';
interface AppActions extends ActionPayloadMap {
userLogin: { userId: string; email: string };
}
interface AppResults {
userLogin: { accepted: boolean };
}
const registry = new ActionRegister<AppActions, AppResults>();
registry.register('userLogin', payload => ({
accepted: payload.email.includes('@'),
}));
const loginResult = await registry.actionsWithResult.userLogin({
userId: '123',
email: 'user@example.com',
});
if (loginResult.outcome === 'completed') {
console.log(loginResult.result);
}ActionResultMap connects each action key to its handler and dispatch result type. A non-blocking handler error is reported as outcome: 'completed_with_errors' while success remains true because the pipeline itself completed. Use outcome when distinguishing complete, partial, failed, cancelled, debounced, and throttled states.
Basic Usage
1. Define Action Types
First, define your action types using the ActionPayloadMap interface:
interface AppActions extends ActionPayloadMap {
userLogin: { userId: string; email: string };
userLogout: void;
processData: { data: any; type: string };
sendNotification: { message: string; userId: string };
resetApp: void;
}2. Create ActionRegister
Create an ActionRegister instance with your action types:
const registry = new ActionRegister<AppActions>({
name: 'MyApp',
registry: { debug: true }
});3. Register Handlers
Register handlers for your actions:
registry.register('userLogin', (payload) => {
console.log('User logged in:', payload.userId, payload.email);
return { success: true };
}, { id: 'login-handler', priority: 100 });
registry.register('userLogout', () => {
console.log('User logged out');
return { success: true };
}, { id: 'logout-handler', priority: 100 });4. Dispatch Actions
Use the actions-based approach to dispatch actions:
// Actions with payload
await registry.actions.userLogin({ userId: '123', email: 'user@example.com' });
await registry.actions.processData({ data: { name: 'test' }, type: 'json' });
await registry.actions.sendNotification({ message: 'Hello!', userId: '123' });
// Actions without payload
await registry.actions.userLogout();
await registry.actions.resetApp();The callable actions and actionsWithResult proxies reserve protocol properties such as then, catch, finally, toJSON, constructor, __proto__, and prototype. These keys are excluded from the proxy type to prevent Promise/serialization protocol collisions. Use dispatch() or dispatchWithResult() directly when a legacy action uses one of these names.
5. Actions with Result Collection
For detailed execution results, use actionsWithResult:
// Actions with result collection
const loginResult = await registry.actionsWithResult.userLogin({
userId: '123',
email: 'user@example.com'
});
console.log('Login Result:', {
success: loginResult.success,
duration: loginResult.execution.duration,
handlersExecuted: loginResult.execution.handlersExecuted,
results: loginResult.results
});
// Actions without payload with result collection
const logoutResult = await registry.actionsWithResult.userLogout();
console.log('Logout Result:', logoutResult);Advanced Features
Options Support
Actions-based dispatching supports all the same options as traditional dispatching:
// With execution options
await registry.actions.processData(
{ data: { name: 'test2' }, type: 'json' },
{ executionMode: 'parallel' }
);
// With debounce
await registry.actions.sendNotification(
{ message: 'Debounced message', userId: '456' },
{ debounce: 100 }
);
// With throttle
await registry.actions.userLogin(
{ userId: '789', email: 'throttled@example.com' },
{ throttle: 1000 }
);Type Safety
The actions-based approach provides excellent type safety:
// ✅ Correct usage - TypeScript will provide autocomplete
await registry.actions.userLogin({ userId: '123', email: 'user@example.com' });
// ❌ TypeScript error - wrong payload structure
// await registry.actions.userLogin({ wrongField: 'value' });
// ❌ TypeScript error - payload not allowed for void actions
// await registry.actions.userLogout({ payload: 'should not exist' });Filtering and Advanced Options
You can use all the advanced filtering and execution options:
// With filtering
await registry.actions.processData(
{ data: { name: 'test' }, type: 'json' },
{
filter: {
handlerIds: ['data-processor'],
priority: { min: 80 }
}
}
);
// With result collection
const result = await registry.actions.processData(
{ data: { name: 'test' }, type: 'json' },
{
result: { collect: true, strategy: 'all' }
}
);Benefits
1. Intuitive Syntax
Actions-based dispatching feels more natural and function-like:
// More intuitive
await registry.actions.userLogin(credentials);
// vs traditional
await registry.dispatch('userLogin', credentials);2. Better Type Safety
Full TypeScript support with autocomplete and compile-time error checking:
// IDE will show autocomplete for action names
registry.actions. // ← Shows: userLogin, userLogout, processData, etc.
// IDE will show autocomplete for payload structure
registry.actions.userLogin({ // ← Shows: userId, email properties3. Consistent API
All actions follow the same pattern regardless of whether they have payloads:
// Actions with payload
await registry.actions.userLogin(payload);
await registry.actions.processData(payload);
// Actions without payload
await registry.actions.userLogout();
await registry.actions.resetApp();4. Full Feature Support
All traditional dispatch features are available:
- Execution modes (sequential, parallel, race)
- Filtering (by handler ID, priority, custom filters)
- Throttling and debouncing
- Result collection
- Abort signals
- Error handling
Migration from Traditional Dispatch
If you're currently using traditional dispatch, migration is straightforward:
// Before
await registry.dispatch('userLogin', { userId: '123', email: 'user@example.com' });
await registry.dispatch('userLogout', undefined);
await registry.dispatch('processData', { data: {} }, { executionMode: 'parallel' });
// After
await registry.actions.userLogin({ userId: '123', email: 'user@example.com' });
await registry.actions.userLogout();
await registry.actions.processData({ data: {} }, { executionMode: 'parallel' });Best Practices
1. Use Descriptive Action Names
Choose action names that clearly describe what they do:
// Good
userLogin, processPayment, sendNotification
// Avoid
action1, doSomething, handle2. Define Clear Payload Types
Use specific types for your payloads:
interface AppActions extends ActionPayloadMap {
userLogin: {
userId: string;
email: string;
rememberMe?: boolean;
};
processPayment: {
amount: number;
currency: string;
paymentMethod: 'card' | 'bank' | 'paypal';
};
}3. Handle Errors Appropriately
Use proper error handling in your handlers:
registry.register('processPayment', async (payload) => {
try {
const result = await paymentService.process(payload);
return { success: true, transactionId: result.id };
} catch (error) {
console.error('Payment failed:', error);
throw error; // Re-throw to let the framework handle it
}
});4. Use Options Wisely
Apply execution options based on your use case:
// For user interactions that might be repeated
await registry.actions.saveDraft(
{ content: draftContent },
{ debounce: 500 }
);
// For expensive operations
await registry.actions.generateReport(
{ reportType: 'monthly' },
{ executionMode: 'parallel' }
);Examples
Complete Example
import { ActionRegister, ActionPayloadMap } from '@context-action/core';
interface ECommerceActions extends ActionPayloadMap {
addToCart: { productId: string; quantity: number };
removeFromCart: { productId: string };
checkout: { paymentMethod: string };
clearCart: void;
}
async function ecommerceExample() {
const registry = new ActionRegister<ECommerceActions>({
name: 'ECommerce',
registry: { debug: true }
});
// Register handlers
registry.register('addToCart', (payload) => {
console.log(`Added ${payload.quantity} of product ${payload.productId} to cart`);
return { success: true };
});
registry.register('removeFromCart', (payload) => {
console.log(`Removed product ${payload.productId} from cart`);
return { success: true };
});
registry.register('checkout', async (payload) => {
console.log(`Processing checkout with ${payload.paymentMethod}`);
// Simulate payment processing
await new Promise(resolve => setTimeout(resolve, 1000));
return { success: true, orderId: 'order-123' };
});
registry.register('clearCart', () => {
console.log('Cart cleared');
return { success: true };
});
// Use actions-based dispatching
await registry.actions.addToCart({ productId: 'prod-1', quantity: 2 });
await registry.actions.addToCart({ productId: 'prod-2', quantity: 1 });
// Checkout with options
const result = await registry.actions.checkout(
{ paymentMethod: 'card' },
{ executionMode: 'parallel' }
);
console.log('Checkout result:', result);
// Clear cart
await registry.actions.clearCart();
}
ecommerceExample().catch(console.error);Actions-based dispatching provides a more intuitive and developer-friendly way to work with Context-Action, while maintaining all the power and flexibility of the traditional approach.