Skip to content

Edit Lifecycle

The Edit Lifecycle is an architectural pattern for managing the render-edit-validate-update cycle in the Arda frontend. It provides a generic, composable framework for draft management, validation, and state transitions across any editable component, from a single text field to a deeply nested domain entity.

Arda domain entities are complex, nested objects. Editing them requires:

  • Draft isolation — changes must not affect the persisted state until explicitly confirmed.
  • Layered validation — a field knows its own format constraints, but a parent knows cross-field and business rules.
  • Error routing — validation errors from any level must reach the correct component for display.
  • Consistent UX — every editable surface (form, grid cell, dialog) should follow the same state machine.
  • Generic over specific — the framework operates on <T>, not on concrete domain types.
  • Composable — validation chains upward; errors route downward by dot-path.
  • Framework-agnostic internals — pure React hooks (useState, useCallback, useMemo, useRef); no external state library required.
  • Opt-in complexity — a leaf component needs only useDraft<T>; parents add composition as needed.
  • Observable — all state is exposed: draft value, validation result, dirty flag, lifecycle phase, and filtered errors.

Every edit session follows a four-phase lifecycle:

PlantUML diagram

Phase descriptions:

PhaseMeaningDraft state
idleNo active edit; displaying confirmed valueDraft equals initialValue
editingUser making changes; validation runs on every updateDraft diverges from initialValue
confirmingUser confirmed; awaiting parent/server acceptanceDraft frozen; onConfirm called
errorConfirm failed validation or parent rejectedDraft preserved for retry
FromToSignalActionDescription
idleeditingupdate() / updateField()Run validate(); call onChangeUser begins editing; draft diverges from initial value
idleidleinitialValue changesReset draft to new initialValueParent refreshed data (e.g., after another component’s save)
editingeditingupdate() / updateField()Run validate(); call onChangeUser continues editing; validation re-runs on every change
editingconfirmingconfirm() [isValid]Call onConfirm(value)User confirms and all validation passes; awaiting parent acceptance
editingerrorconfirm() [!isValid]Set phase to errorUser confirms but validation fails; draft preserved for correction
editingidlecancel()Reset draft to initialValue; call onCancelUser abandons changes; draft reverts
confirmingidleinitialValue changesReset draft to new initialValueParent accepted the confirmed value and propagated it back
confirmingerrorcontextErrors injectedMerge errors into allErrorsParent rejected (e.g., server-side validation failed); errors displayed
erroreditingupdate() / updateField()Run validate(); call onChangeUser edits to fix the validation errors
erroridlecancel()Reset draft to initialValue; call onCancelUser abandons the failed edit

Contextual errors propagate from parent to child in two distinct forms. The choice depends on whether the parent can evaluate the constraint immediately after a single child change, or only once all children have settled.

The parent detects an invalid cross-child state as soon as a child reports a change. The parent injects contextErrors into the child immediately, and the child displays them alongside its own intrinsic errors. The child cannot confirm while blocking contextual errors are present.

Example: a Supplier editor rejects a child Address whose postal code falls outside the supplier’s serviceable region. The parent knows this the instant the postal code changes.

PlantUML diagram

The parent cannot evaluate the constraint from a single child change because the constraint spans multiple children. The invalid state is transient — the user may be mid-way through a multi-field edit and will resolve it before confirming the parent. The parent issues a warning (not a blocking error) during editing, and only promotes it to a blocking error when the user attempts to confirm the parent itself.

Example: a Date editor with independent Day and Month fields. Changing the month from March to February makes “February 30” temporarily invalid, but the user intends to change the day next. The parent shows a warning during editing and blocks confirm only if the user tries to submit without fixing it. The error is displayed at the parent level because it stems from the combined state of multiple children, not from any single child’s edit.

PlantUML diagram

CriterionImmediateDelayed
Constraint scopeSingle child, evaluable on each changeMulti-child, evaluable only on combined state
Error injection targetChild’s contextErrors propParent’s own error display
Blocks child confirmYes (if severity is 'error')No — child is unaware of the constraint
Blocks parent confirmIndirectly (child cannot confirm)Yes — parent’s own validation fails
User experienceChild shows error inline immediatelyParent shows warning during editing, error on confirm attempt
ExamplePostal code outside serviceable region”February 30” from independent day/month fields

Validation operates in two layers:

The component validates its own value based on its nature. This runs on every draft change via the validate function passed to useDraft<T>.

Examples: required fields, format checks (postal code pattern), range constraints (quantity > 0).

The parent validates the child’s value in the context of siblings or business rules. Contextual errors are injected into the child via the contextErrors prop and merged into the child’s allErrors.

Examples: date range validation (start < end), uniqueness across siblings, business rules that span multiple fields.

interface FieldError {
field: string; // Dot-path: "street", "address.postalCode"
message: string; // Human-readable
code?: string; // Machine-readable for i18n and testing
severity?: 'error' | 'warning';
}
  • Errors with severity: 'error' (or no severity) block confirm.
  • Errors with severity: 'warning' are displayed but do not block.

The errorsFor(field) function filters allErrors by prefix matching: errorsFor('address') returns errors for 'address', 'address.city', 'address.postalCode', but not 'addressLine2'.

This enables parent components to route errors to the correct child without each child needing to know its position in the hierarchy.


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