Why this matters
A SAP Business One Business Partner editor can fail even when every input works. A user changes CreditLimit, an autosave overwrites a colleague’s change, the page shows success, and the record still cannot be submitted because its CardCode is invalid. That is not primarily an input-component problem.
A form is a temporary workspace for a business transaction. Before choosing React Hook Form, TanStack Form, Angular forms, or plain state, identify the transaction, its owner, and its rules. The same reasoning applies to every frontend framework.
Who this is for
Mid-level through staff-level frontend engineers, technical leads, and architects designing or reviewing CRUD workflows in any frontend framework.
Decision checklist
Before designing fields, answer these questions in writing:
- What business outcome is being changed? “Save Business Partner” is vague; “change
CreditLimit,EmailAddress, and one Bill-toBPAddressesrow forCardCodeC20000” is a transaction. - What is the commit boundary? Can a Business Partner’s credit setting save independently of a Business Partner Group master-data change? Usually yes.
- Who owns draft state? Usually one form controller owns it. A reusable Business Partner Group selector owns presentation, not the parent transaction.
- Which rules can run locally, and which require current server data? A required
CardName,EmailAddress, orBPAddresses[].AddressNamediffers from “thisCardCodeis already assigned.” - What happens on save, failure, discard, and concurrent change? These are states, not afterthoughts.
- Is the user editing one transaction or several? A wizard may be one transaction; two independent panels should not share a fake parent form.
If the team cannot name the transaction boundary, do not start with a form library. Resolve the product workflow first.
Common anti-patterns
| Pattern | Why it looks reasonable | Long-term consequence | Better boundary |
|---|---|---|---|
| Parent knows every child field | It centralizes Save | Parent changes for every section change | Parent owns submit orchestration; sections expose a focused value contract |
| Child writes parent state directly | It removes a callback | Hidden coupling and impossible reuse | Child emits an intent or binds to a scoped field API |
| Duplicate draft state | Local state feels convenient | Conflicting values and reset bugs | One draft source of truth |
useEffect copies dirty state | It appears to keep values current | Races, loops, and lost edits | Derive UI from form state; reset only at explicit lifecycle boundaries |
| Rules scattered across components | Each field can “own” its check | Inconsistent messages and missed server checks | Put rules beside the domain shape and orchestrate their timing centrally |
| API call inside reusable input | The input has the value | UI primitives acquire business dependencies | Keep inputs presentational; inject async behavior from the form feature |
| One giant form for unrelated work | One Save button seems simpler | A small change blocks unrelated work | Split independent transactions; coordinate only when the business requires it |
| Separate forms for one order | Sections were built by different teams | Partial saves and cross-section invalidity | One transaction owner with composable sections |
A useful distinction: data rules versus workflow rules
“CreditLimit cannot be negative” is a data rule. “Raising CreditLimit above an approval threshold needs manager approval” is a workflow rule. The first should make the draft invalid. The second may be valid data that follows a different submit path. Treating both as red field errors makes the UI misleading.
Validation is a lifecycle, not a function
Validation answers whether a proposed value is acceptable at a particular boundary. Its timing changes the user experience and the cost of a request.
| Mode | Run when | Best for | Do not use as the only check when |
|---|---|---|---|
| On change | Input changes | Cheap format checks after a field has been touched | The rule requires a network call or interrupts typing |
| On blur | User leaves a field | Availability, normalization, focused feedback | The user can submit without visiting the field |
| On submit | Commit is requested | Complete transaction validation | Feedback must guide the user earlier |
| Async | External data is needed | Uniqueness, eligibility, current pricing | A stale response could overwrite a newer value |
| Cross-field | Related values change | Date ranges, totals, conditional requirements | Fields are actually separate transactions |
| Section-level | A user completes a step | Wizard progression | The final transaction has broader invariants |
| Server | Every write | Authorization, freshness, authoritative constraints | It is used to replace basic client guidance |
Client validation improves correction speed. Server validation protects the transaction. Neither is sufficient alone.
On server failure, retain the draft, map field-specific errors to their fields, show a form-level error for transaction failures, and restore the Save action once the request finishes. Do not show success until the server has accepted the transaction. Discard should return to the last acknowledged snapshot, not whatever values happened to be loaded first.
Incremental validation: validate the smallest affected scope
For a Business Partner maintenance screen with 500 editable rows, validating every row on each keystroke wastes work and makes unrelated errors jump into view. Validate the changed field first, then its dependent rules: changing GroupCode may revalidate CardType compatibility, but not a read-only CurrentAccountBalance.
This is appropriate when dependencies are explicit and validation is pure. Use a full validation on submit anyway. Prefer broader validation when a rule depends on global derived state, permissions, or a server snapshot that may have changed. Incremental validation is an optimization; it must not alter what a valid submitted transaction means.
What good validation looks like
At the field level, a message names the problem and the correction: “Enter a tax ID with 10 digits,” not “Invalid.” It is attached accessibly to the field and does not appear before the user has a reasonable chance to act.
At the form level, rules have a stable home, validation status is observable, and submit focuses the first actionable error. A section should be able to report its validity without owning the whole transaction.
At the application level, the client and server share compatible rule definitions or contracts, error codes are observable, permissions are enforced on the server, and teams can explain the source of truth. Measure validation failures by rule and endpoint; repeated failures often reveal a product or data-quality problem, not careless users.
Key takeaways
- Start with the business transaction, then choose the form architecture.
- Keep one authoritative draft per transaction; derive UI rather than synchronizing copies.
- Match validation timing to cost, dependency, and user intent.
- Validate incrementally for feedback, then validate the complete transaction before commit.
- Keep server validation authoritative and preserve the user’s draft on failure.
Conclusion
The first architectural decision is not which hook to call. It is whether the screen represents one transaction, who owns its draft, and when the system is allowed to accept it. With those answers, a form library becomes an implementation tool instead of the place where business design accidentally happens.