React dashboard: a stale tab's save reverts translations another tab saved (every persisted row is submitted)
Describe the bug
In the React dashboard, a detail page left open in one browser tab overwrites the translations another tab saved in the meantime, including languages the stale tab never touched. Each save submits every persisted translation row as it was when that tab loaded, and the server writes each row it receives, so the last save wins for every language.
To Reproduce
Versions: @vendure/dashboard 3.7.3, react-hook-form 7.74.0 (as resolved by the dashboard).
- Take a Product that has an NL and an EN translation.
- Open it in two tabs: A and B.
- In tab B, switch the content language to English, change the EN name, and click Update.
- In tab A (loaded before step 3, content language Dutch), change the NL name and click Update.
- Reload.
Expected: EN has tab B's name, NL has tab A's name.
Actual: NL has tab A's name, but EN is back to the name tab A loaded with; tab B's save is lost. The UpdateProduct input sent by tab A contains every persisted translation (NL edited, EN and DE unchanged from load time).
Mechanisms (as far as I can tell)
stripUntouchedTranslations(#4962, fix for #4885) only drops rows that are unpersisted and untouched, so every persisted row is still submitted whole, touched or not.- The QueryClient in
app-providers.tsxsetsrefetchOnWindowFocus: false, so a tab that has been open a while keeps its load-time rows even when it has no edits. use-generated-form.tsxseedsuseFormthroughvalues(noresetOptions), so the form only picks up another tab's save when the detail query itself refetches.
The legacy Angular admin reportedly sent only edited languages (per #4962's description), which would have avoided this.
Workaround we use
In our own detail pages built with useDetailPage, transformUpdateInput narrows the update input to what this tab changed: a translation row is kept only when it differs from the form's baseline (formState.defaultValues), matched by languageCode, and other top-level keys are dropped when unchanged. We also call refreshEntity() when an unchanged tab regains focus or visibility. This covers our pages only; stock pages (Product, Collection, Facet, …) still show the behaviour above. transformUpdateInput is undocumented and useDetailPage is marked Developer Preview, so we're aware we're leaning on internals.
Possible directions
Not prescribing one, just the options we considered:
- send only changed translation rows (and fields) by default, or
- refetch clean detail pages on window focus.
Either would stop a stale tab from reverting languages it never touched. Two tabs editing the same language would still collide; that seems like a separate optimistic-locking question.
Source: vendurehq/vendure