콘텐츠로 이동

Intent는 사용자 액션에서 하나 이상의 Command로의 선언적 매핑이에요. UI에서 호출하는 것 — Hurum 데이터 흐름의 진입점이에요.

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, ...)는 순차 Intent를 생성해요. 트리거되면 각 Command의 Executor를 순서대로 실행해요. 하나가 throw하면 나머지는 건너뛰어요.

Intents()Events()가 Event를 그룹화하는 것과 비슷하게, 관련된 Intent를 네임스페이스로 그룹화해요. 각 키는 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({})

send에 전달하는 페이로드는 Intent의 모든 Command에 전달돼요. TypeScript가 컴파일 시점에 페이로드 타입이 모든 Command와 호환되는지 확인해요.

Hurum은 여러 Command를 실행하기 위한 세 가지 전략을 제공해요:

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,
),
})
전략실행 방식에러 발생 시
Intent(A, B, C)A, 그 다음 B, 그 다음 C중단. B와 C는 실행되지 않음.
Intent.all(A, B, C)A, B, C 동시 실행나머지를 중단.
Intent.allSettled(A, B, C)A, B, C 동시 실행나머지는 계속 실행.

Intent는 자동 및 수동 취소를 지원해요:

재실행 시 자동 취소. 같은 Intent를 다시 보내면 이전 실행을 중단해요. 입력하면서 검색하는 기능에 유용해요: 각 키 입력이 이전 검색을 취소해요.

// 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' })

수동 취소. 특정 Intent에는 store.cancel(ref)을, 모든 것에는 store.cancelAll()을 사용하세요:

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

Executor가 취소되면 signal.abortedtrue가 되고, 그 이후에 emit된 Event는 조용히 무시돼요.

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),
})

자동 취소 동작 덕분에 기본적인 경우에는 별도의 디바운스 유틸리티가 필요 없어요:

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

진정한 디바운스(실행 전 N ms 대기)가 필요하다면, Executor 자체에서 처리하세요.

  • Intent 이름은 사용자 액션으로 지정하세요. plusClicked, formSubmitted, pageOpenedincrementCountersaveData가 아니에요. Intent는 사용자가 한 일을 설명하는 거지, 시스템이 해야 할 일을 설명하는 게 아니에요.
  • 독립적인 로드에는 Intent.allSettled를 사용하세요. 통화, VAT 세율, 구매 데이터를 로드하는 것이 독립적이라면, 하나의 실패가 나머지를 차단하지 않게 하세요.
  • 사용자 액션당 하나의 Intent. 버튼 클릭이 유효성 검사 후 저장을 해야 한다면, 두 개의 Command를 가진 하나의 Intent이지, 두 개의 Intent가 아니에요.
  • Intent는 정확히 하나의 Store에 속해요. Store 빌더에서 .intents()로 Intent를 등록해요. 같은 Intent 네임스페이스를 여러 Store에서 공유할 수 없어요.