Skip to content

An Intent is a declarative mapping from a user action to one or more commands. It is what you call from your UI — the entry point into Hurum’s data flow.

import { Intents, Intent } from '@hurum/core'
const PurchaseIntents = Intents('Purchase', {
submitButtonClicked: Intent(ValidateCommand, SavePurchaseCommand),
pageOpened: Intent(LoadPurchaseCommand),
cancelClicked: Intent(CancelCommand),
})
// Send from your UI
PurchaseStore.send.submitButtonClicked({ id: '123' })

Intent(Command1, Command2, ...) creates a sequential intent. When triggered, it runs each command’s executor in order. If one throws, the rest are skipped.

Intents() groups related intents under a namespace, similar to how Events() groups events. Each key becomes a method on store.send:

const CounterIntents = Intents('Counter', {
plusClicked: Intent(IncrementCommand),
minusClicked: Intent(DecrementCommand),
resetClicked: Intent(ResetCommand),
})
// After registering with a Store:
CounterStore.send.plusClicked({ amount: 1 })
CounterStore.send.resetClicked({})

The payload you pass to send is forwarded to every command in the intent. TypeScript checks that the payload type is compatible with all commands at compile time.

Hurum provides three strategies for running multiple commands:

const AppIntents = Intents('App', {
// Sequential (default) -- run one after another, stop on first error
submit: Intent(ValidateCommand, SaveCommand),
// Parallel fail-fast -- run all at once, abort others on first error
quickSubmit: Intent.all(ValidateCommand, SaveCommand),
// Parallel independent -- run all at once, each completes regardless
initialize: Intent.allSettled(
LoadCurrencyCommand,
LoadVATCommand,
LoadPurchaseCommand,
),
})
StrategyExecutionOn error
Intent(A, B, C)A, then B, then CStop. B and C don’t run.
Intent.all(A, B, C)A, B, C simultaneouslyAbort the others.
Intent.allSettled(A, B, C)A, B, C simultaneouslyOthers continue.

Intents support automatic and manual cancellation:

Auto-cancel on re-execution. Sending the same intent again aborts the previous run. This is useful for search-as-you-type: each keystroke cancels the previous search.

// The first call starts running
PurchaseStore.send.submitButtonClicked({ id: '123' })
// This aborts the first call and starts a new one
PurchaseStore.send.submitButtonClicked({ id: '456' })

Manual cancel. Use store.cancel(ref) for a specific intent or store.cancelAll() for everything:

// Cancel a specific intent
const ref = PurchaseStore.send.submitButtonClicked({ id: '123' })
PurchaseStore.cancel(ref)
// Cancel all running executors
PurchaseStore.cancelAll()

When an executor is cancelled, its signal.aborted becomes true and any events emitted after that point are silently ignored.

const PageIntents = Intents('ProductPage', {
// Load everything the page needs
opened: Intent.allSettled(
LoadProductCommand,
LoadReviewsCommand,
LoadRecommendationsCommand,
),
// Clean up on leave
closed: Intent(CleanupCommand),
})
const FormIntents = Intents('ContactForm', {
// Validate first, then submit
submitted: Intent(ValidateFormCommand, SubmitFormCommand),
// Individual field changes
fieldChanged: Intent(UpdateFieldCommand),
})

The auto-cancel behavior means you don’t need a separate debounce utility for basic cases:

const SearchIntents = Intents('Search', {
queryChanged: Intent(SearchCommand),
})
// Each call cancels the previous one
SearchStore.send.queryChanged({ query: 'h' })
SearchStore.send.queryChanged({ query: 'hu' })
SearchStore.send.queryChanged({ query: 'hur' }) // only this one completes

For true debounce (waiting N ms before executing), handle it in the executor itself.

  • Name intents after user actions. plusClicked, formSubmitted, pageOpened — not incrementCounter or saveData. The intent describes what the user did, not what the system should do.
  • Use Intent.allSettled for independent loads. If loading currencies, VAT rates, and purchase data are independent, don’t let one failure block the others.
  • One intent per user action. If a button click should validate then save, that’s one intent with two commands, not two intents.
  • Intents belong to exactly one Store. You register intents with .intents() in the Store builder. The same intent namespace cannot be shared across stores.