Skip to content

Edit Lifecycle — Type Catalog and Hook Reference

Type definitions and hook API for the Edit Lifecycle framework. See Edit Lifecycle for the pattern overview and state machine.

PlantUML diagram

A single validation error on a specific field.

PropertyTypeRequiredDescription
fieldstringYesDot-path to the field (e.g., "postalCode", "address.city")
messagestringYesHuman-readable error message
codestringNoMachine-readable code for testing and i18n
severity'error' | 'warning'No'error' blocks confirm; 'warning' shows but allows. Default: 'error'

Result of validating a value.

PropertyTypeDescription
validbooleantrue if no errors (warnings are allowed)
errorsFieldError[]List of field-level errors and warnings

A function that validates a value and returns a result: (value: T) => ValidationResult.

Convention: each editable component exports its validator as a named function alongside the component (e.g., validateAddress).

Union of the four lifecycle phases: 'idle' | 'editing' | 'confirming' | 'error'.

Callbacks for the edit lifecycle, all optional:

CallbackSignatureWhen called
onChange(value: T, validation: ValidationResult) => voidEvery draft change
onConfirm(value: T) => voidUser confirms and intrinsic validation passes
onCancel() => voidUser cancels the edit

Standard props for any editable component. Extends EditLifecycleCallbacks<T>.

PropertyTypeRequiredDescription
initialValueTYesStarting value from parent or data provider
contextErrorsFieldError[]NoErrors injected by the parent
disabledbooleanNoDisables editing (parent saving, sibling editing)

The core hook that implements the state machine. Each editable component calls useDraft to manage its local draft.

interface UseDraftOptions<T> {
initialValue: T;
validate: Validator<T>;
contextErrors?: FieldError[];
onChange?: (value: T, validation: ValidationResult) => void;
onConfirm?: (value: T) => void;
onCancel?: () => void;
isEqual?: (a: T, b: T) => boolean;
}
OptionDescription
initialValueResets the draft when this value changes
validateIntrinsic validator, called on every draft change
contextErrorsParent-injected errors, merged into allErrors
onChangeForwarded lifecycle callback
onConfirmForwarded lifecycle callback
onCancelForwarded lifecycle callback
isEqualCustom equality for initialValue change detection. Default: Object.is (reference equality)
interface DraftState<T> {
value: T;
intrinsicValidation: ValidationResult;
allErrors: FieldError[];
dirty: boolean;
isValid: boolean;
phase: EditPhase;
update: (updater: T | ((prev: T) => T)) => void;
updateField: (path: string, value: unknown) => void;
confirm: () => void;
cancel: () => void;
reset: () => void;
errorsFor: (field: string) => FieldError[];
}
MemberDescription
valueCurrent draft value
intrinsicValidationLatest result from the validate function
allErrorsIntrinsic errors + contextErrors, for display
dirtytrue when draft differs from initialValue
isValidtrue when no blocking errors (intrinsic + contextual)
phaseCurrent lifecycle phase
update(updater)Replace draft; accepts value or updater function
updateField(path, value)Update a single field by dot-path
confirm()Confirm if valid (calls onConfirm); else enter error phase
cancel()Reset to initialValue and call onCancel
reset()Reset to initialValue without calling onCancel
errorsFor(field)Filter allErrors by field prefix

Initial value reset. The draft resets when initialValue changes. By default this uses reference equality (Object.is). Callers must memoize initialValue to prevent unwanted resets. Provide isEqual for structural comparison when needed.

Validation on every change. update() and updateField() run the validate function synchronously and call onChange with the result.

Error merging. allErrors is the union of intrinsic validation errors and contextErrors. isValid is false when any error has severity: 'error' (or no severity). Warnings do not block confirm.

Prefix-based error filtering. errorsFor('address') matches 'address', 'address.city', 'address.postalCode' but not 'addressLine2'. This enables automatic error routing in nested hierarchies.

function AddressEditor(props: EditableComponentProps<Address>) {
const draft = useDraft({
initialValue: props.initialValue,
validate: validateAddress,
contextErrors: props.contextErrors,
onChange: props.onChange,
onConfirm: props.onConfirm,
onCancel: props.onCancel,
});
return (
<div>
<input
value={draft.value.street}
onChange={(e) => draft.updateField('street', e.target.value)}
/>
{draft.errorsFor('street').map((e) => (
<span key={e.message}>{e.message}</span>
))}
<button onClick={draft.confirm} disabled={!draft.isValid}>
Save
</button>
<button onClick={draft.cancel}>Cancel</button>
</div>
);
}
// Types only (no React dependency)
import type {
FieldError,
ValidationResult,
Validator,
EditPhase,
EditLifecycleCallbacks,
EditableComponentProps,
} from '@arda-cards/design-system/types/canary';
// Hook + types (requires React)
import { useDraft, setNestedField } from '@arda-cards/design-system/canary';
import type { UseDraftOptions, DraftState } from '@arda-cards/design-system/canary';

Copyright: (c) Arda Systems 2025-2026, All rights reserved