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.
Problem
Section titled “Problem”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.
Design Principles
Section titled “Design Principles”- 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.
State Machine
Section titled “State Machine”Every edit session follows a four-phase lifecycle:
Phase descriptions:
| Phase | Meaning | Draft state |
|---|---|---|
idle | No active edit; displaying confirmed value | Draft equals initialValue |
editing | User making changes; validation runs on every update | Draft diverges from initialValue |
confirming | User confirmed; awaiting parent/server acceptance | Draft frozen; onConfirm called |
error | Confirm failed validation or parent rejected | Draft preserved for retry |
Transition Reference
Section titled “Transition Reference”| From | To | Signal | Action | Description |
|---|---|---|---|---|
idle | editing | update() / updateField() | Run validate(); call onChange | User begins editing; draft diverges from initial value |
idle | idle | initialValue changes | Reset draft to new initialValue | Parent refreshed data (e.g., after another component’s save) |
editing | editing | update() / updateField() | Run validate(); call onChange | User continues editing; validation re-runs on every change |
editing | confirming | confirm() [isValid] | Call onConfirm(value) | User confirms and all validation passes; awaiting parent acceptance |
editing | error | confirm() [!isValid] | Set phase to error | User confirms but validation fails; draft preserved for correction |
editing | idle | cancel() | Reset draft to initialValue; call onCancel | User abandons changes; draft reverts |
confirming | idle | initialValue changes | Reset draft to new initialValue | Parent accepted the confirmed value and propagated it back |
confirming | error | contextErrors injected | Merge errors into allErrors | Parent rejected (e.g., server-side validation failed); errors displayed |
error | editing | update() / updateField() | Run validate(); call onChange | User edits to fix the validation errors |
error | idle | cancel() | Reset draft to initialValue; call onCancel | User abandons the failed edit |
Activity Flow
Section titled “Activity Flow”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.
Immediate Contextual Errors
Section titled “Immediate Contextual Errors”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.
Delayed Contextual Errors
Section titled “Delayed Contextual Errors”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.
Choosing Between Immediate and Delayed
Section titled “Choosing Between Immediate and Delayed”| Criterion | Immediate | Delayed |
|---|---|---|
| Constraint scope | Single child, evaluable on each change | Multi-child, evaluable only on combined state |
| Error injection target | Child’s contextErrors prop | Parent’s own error display |
| Blocks child confirm | Yes (if severity is 'error') | No — child is unaware of the constraint |
| Blocks parent confirm | Indirectly (child cannot confirm) | Yes — parent’s own validation fails |
| User experience | Child shows error inline immediately | Parent shows warning during editing, error on confirm attempt |
| Example | Postal code outside serviceable region | ”February 30” from independent day/month fields |
Validation Model
Section titled “Validation Model”Validation operates in two layers:
Intrinsic validation
Section titled “Intrinsic validation”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).
Contextual validation
Section titled “Contextual validation”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.
Error structure
Section titled “Error structure”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.
Error routing by dot-path
Section titled “Error routing by dot-path”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.
Further Reading
Section titled “Further Reading”- Type Catalog and Hook Reference — all
types,
useDraft<T>interface, and usage examples. - Hierarchical Composition — how components compose, data flow patterns, and relationship to FD-01 typed data providers.
- Deferred framework extensions:
ux-prototype#77
(
useComposedDraft<T>,createCellEditorFactory<T>,EditablePanel<T>).
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved