React
Preserve a CRM Form Draft When Fresh Server Data Arrives
By Vivek Jalondhara · 5 min read
Published
Keep local edits, compare fresh server values with the editing baseline, and expose conflicts without resetting a CRM form.
A user changes a CRM contact’s title, switches tabs, and returns to find their unfinished text replaced by a background response. The request succeeded. The editing experience failed because the application treated a newer server snapshot as permission to replace a local draft.
This educational SmackDab CRM scenario follows one open record through a refetch and a conflicting save. It concerns record values rather than form-schema migration. The proposed merge policy is illustrative and does not describe the CRM’s private implementation.
Keep three values with different jobs
The baseline is the server snapshot from which editing began. The draft contains the user’s current choices. An incoming snapshot is a candidate update from the server. These values may contain identical text, but they answer different questions and should not silently replace one another.
For example, the baseline title is Coordinator, the draft says Senior Coordinator, and an incoming record changes only the telephone number. Preserving every old value would hide the new number. Replacing every value would erase the new title. A field-level comparison can retain both meaningful changes.
Merge fields that have an unambiguous outcome
For a simple text field, compare local and remote values with the baseline. If the user left the field unchanged, accept the remote value. If only the user changed it, keep the local value. If both sides independently reached the same value, there is no conflict to resolve.
The example implements those rules for strings. Arrays, attachments, and related records need their own policies. Matching two arrays by position is unsafe when another user can reorder or delete items. Use stable item identities and distinguish removal from an empty value before generalising the helper.
Keep conflicting text visible
When local and remote values both differ from the baseline and from each other, retain the local text and show the remote alternative. Explain the choice with field names and values. An unexplained toast followed by a reset makes the user reconstruct work from memory.
Do not mark the conflict resolved just because another refetch returns the same remote snapshot. Preserve the unresolved comparison until the person chooses a value or explicitly discards the draft. After reconciliation, advance the baseline deliberately and recalculate which fields still represent unsaved changes.
Tie draft ownership to record identity
A draft belongs to a tenant and record, not merely to whichever editor component happens to be mounted. Switching to a different record should load that record’s editing session or explicitly close the previous one. Reusing an old draft under a new heading creates a more serious problem than a lost keystroke.
React’s guidance on state structure and avoiding unnecessary Effects helps identify accidental copies of props. An unconditional effect that copies every server response into editable state encodes a replacement policy, whether intended or not. Put the reconciliation decision at the data-arrival boundary and keep ordinary rendering free of that decision.
Carry the comparison through the save
Preserving text in the browser does not prevent lost updates on the server. Submit a revision or a conditional request validator with the change. HTTP If-Match is one standardized way to make a write depend on the representation that the editor previously read; a failed precondition must remain a recoverable outcome.
When a save is rejected, retain the draft and compare it with the latest authorised record. Avoid automatically resubmitting against the newest revision because that converts a conflict into an overwrite. A successful response should establish the next baseline, including any normalisation the server applied.
Verify the situations that erase work
Exercise a refetch while typing, a remote change to a clean field, competing changes to one dirty field, and switching records before a delayed response arrives. Check values, dirty indicators, and conflict messages together. A test that asserts only the final API response can miss the discarded draft.
Also verify an intentional discard. Protecting drafts should not trap a person in an unwanted editing session. Make the discard action explicit, then reset the baseline and values together so the next edit starts from a coherent record.
Reconcile one text field without erasing the draft
This pure helper operates on three already validated strings. The editor remains responsible for retaining the baseline, presenting conflicts, and saving against an authorised record revision.
Illustrative example · TypeScript · Simplified for explanation
type TextMerge =
| { value: string; conflict: false }
| { value: string; conflict: true; remote: string };
function reconcileText(base: string, local: string, remote: string): TextMerge {
if (local === base) return { value: remote, conflict: false };
if (remote === base || local === remote) {
return { value: local, conflict: false };
}
return { value: local, conflict: true, remote };
}- Retain the editing baseline
- Compare the incoming record with the draft
- Merge safe fields and expose conflicts
- Save against an explicit revision
Conceptual flow for the example above.
Tradeoff to keep in mind
Field-level merging preserves useful remote updates, but complex values need domain-specific rules. A deliberate reload can be simpler for forms where partial merging would obscure the user’s intent.