ref(settings): Switch OwnerInput to useMutation with fetchMutation#111087
ref(settings): Switch OwnerInput to useMutation with fetchMutation#111087JoshuaKGoldberg wants to merge 4 commits intomasterfrom
Conversation
78371b1 to
fa75cda
Compare
Replace manual Client + requestPromise with useMutation/fetchMutation hook pattern. Derive hasChanges from mutation state instead of tracking it as independent boolean state, comparing current text against the last successfully saved value from mutation.data. Refs LINEAR-ENG-7011 Co-Authored-By: Claude Sonnet 4 <noreply@example.com> Made-with: Cursor
fa75cda to
f492a6a
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| }, | ||
| }); | ||
|
|
||
| const hasChanges = (text ?? initialText) !== (mutation.data?.raw ?? initialText); |
There was a problem hiding this comment.
Derived hasChanges breaks retry after failed save
Medium Severity
In TanStack Query v5, mutation.data is reset to undefined when mutate() is called again. This means if a user previously saved successfully, then edits text back to match initialText, and attempts to save again, mutation.data?.raw ?? initialText falls back to initialText — which equals the current text — so hasChanges becomes false. If that save fails, the user is stuck: the Save button is disabled and they cannot retry. The old code unconditionally set hasChanges = false only on success and true on any edit, avoiding this issue entirely.
There was a problem hiding this comment.
@TkDodo I would normally solve this by setting a piece of state like "hasChanges" again, which is the opposite of #110200 (comment). Do you have advice?
There was a problem hiding this comment.
hasChanges is only used to disable the button if the text is “the same”. Our UX guidelines usually don’t want us to disable buttons at all, our form system also always allows submission.
What you can do is:
- use our new form system here (
useScrapsForm) instead of local state withtext. I know it’s just one field but it’s still a form. - compare against
initialTextonly. This is the data that comes from the API response, so the flow is like this:- query delivers
initialText - user changes it
- submits, makes a mutation
- mutation invalidates the query or updates the cache
- one problem I see is that doesn’t happen in all instances. Like here, we don’t implement
onSaveso the query never gets updated
- one problem I see is that doesn’t happen in all instances. Like here, we don’t implement
- the parent component that has the query re-renders
- this component receives a new
initialText, that now has the latest value you want to compare against
- query delivers
| }); | ||
| }, | ||
| onError: (caught: RequestError) => { | ||
| setError(caught.responseJSON as InputError); |
There was a problem hiding this comment.
do we need this? why not just use the error state returned from the mutation:
const { error } = useMutation(...)
that state re-sets automatically if the mutation is fired again.
| return request; | ||
| const handleUpdateOwnership = () => { | ||
| setError(null); | ||
| mutation.mutate(); |
There was a problem hiding this comment.
we’d probably want to pass the text here so that it’s available as variables in the mutation. feels a bit better than closing over text
| }, | ||
| }); | ||
|
|
||
| const hasChanges = (text ?? initialText) !== (mutation.data?.raw ?? initialText); |
There was a problem hiding this comment.
hasChanges is only used to disable the button if the text is “the same”. Our UX guidelines usually don’t want us to disable buttons at all, our form system also always allows submission.
What you can do is:
- use our new form system here (
useScrapsForm) instead of local state withtext. I know it’s just one field but it’s still a form. - compare against
initialTextonly. This is the data that comes from the API response, so the flow is like this:- query delivers
initialText - user changes it
- submits, makes a mutation
- mutation invalidates the query or updates the cache
- one problem I see is that doesn’t happen in all instances. Like here, we don’t implement
onSaveso the query never gets updated
- one problem I see is that doesn’t happen in all instances. Like here, we don’t implement
- the parent component that has the query re-renders
- this component receives a new
initialText, that now has the latest value you want to compare against
- query delivers
Made-with: Cursor
|
👋 @TkDodo I've got a bunch of logs work on my plate and don't have the time to continue this. The changes are here if you/yours want them. Thanks for the advice, it was really helpful! |


Switch OwnerInput from manual
new Client()+api.requestPromisetouseMutation/fetchMutation, and derivehasChangesas a computed variable instead of independent boolean state.Follow-up cleanup from #110200 based on review feedback. The
useMutationhook replaces the manual.then()/.catch()chain with declarativeonSuccess/onErrorcallbacks.hasChangesis now derived by comparing the current text againstmutation.data?.raw(the last successfully saved value), falling back toinitialText.Fixes ENG-7011
Made with Cursor