@@ -19,6 +19,7 @@ import type {
1919 TriggerWithoutArgs ,
2020 TriggerWithOptionsArgs
2121} from './types'
22+ import { useTransition } from './use-transition'
2223
2324const mutation = ( < Data , Error > ( ) =>
2425 (
@@ -36,13 +37,28 @@ const mutation = (<Data, Error>() =>
3637 const [ stateRef , stateDependencies , setState ] = useStateWithDeps < {
3738 data : Data | undefined
3839 error : Error | undefined
39- isMutating : boolean
4040 } > ( {
4141 data : UNDEFINED ,
42- error : UNDEFINED ,
43- isMutating : false
42+ error : UNDEFINED
4443 } )
4544
45+ // https://github.com/vercel/swr/issues/4247
46+ //
47+ // In short, when `trigger` is called with in a transition (e.g. React's <form action /> action prop),
48+ // any state update inside the transtion (a.k.a. the `trigger` function) will be deferred/delayed until
49+ // the transition finishes, which means async function resolved/rejected.
50+ //
51+ // However, we don't want `isMutating` update (false -> true -> false) to be deferred/delayed, otherwise
52+ // the UI won't be able to reflect the loading state.
53+ //
54+ // One way to do this is to use `useTransition`. In React 19, `useTransition`'s `isPending` can be used to
55+ // track async transition resolved/rejected state. And `isPending` would be an urgent update that won't be
56+ // deferred/delayed.
57+ //
58+ // React 18's `useTransition` doesn't support async function tracking. In React 16 and 17, there is no
59+ // `useTransition` at all. A polyfill will be used.
60+ const [ isMutating , startMutation ] = useTransition ( )
61+
4662 const currentState = stateRef . current
4763
4864 const trigger = useCallback (
@@ -71,31 +87,37 @@ const mutation = (<Data, Error>() =>
7187
7288 ditchMutationsUntilRef . current = mutationStartedAt
7389
74- setState ( { isMutating : true } )
90+ const mutatePromise = mutate < Data > (
91+ serializedKey ,
92+ ( fetcherRef . current as any ) ( resolvedKey , { arg } ) ,
93+ // We must throw the error here so we can catch and update the states.
94+ mergeObjects ( options , { throwOnError : true } )
95+ )
96+
97+ // startTransition returns void, so we can only use it to track the async function state
98+ startMutation ( async ( ) => {
99+ try {
100+ await mutatePromise
101+ } catch {
102+ // ignore error in transition state tracking
103+ }
104+ } )
75105
76106 try {
77- const data = await mutate < Data > (
78- serializedKey ,
79- ( fetcherRef . current as any ) ( resolvedKey , { arg } ) ,
80- // We must throw the error here so we can catch and update the states.
81- mergeObjects ( options , { throwOnError : true } )
82- )
107+ // actually get result from the mutation promise and handle potential error
108+ const data = await mutatePromise
83109
84110 // If it's reset after the mutation, we don't broadcast any state change.
85111 if ( ditchMutationsUntilRef . current <= mutationStartedAt ) {
86- startTransition ( ( ) =>
87- setState ( { data, isMutating : false , error : undefined } )
88- )
112+ startTransition ( ( ) => setState ( { data, error : undefined } ) )
89113 options . onSuccess ?.( data as Data , serializedKey , options )
90114 }
91115 return data
92116 } catch ( error ) {
93117 // If it's reset after the mutation, we don't broadcast any state change
94118 // or throw because it's discarded.
95119 if ( ditchMutationsUntilRef . current <= mutationStartedAt ) {
96- startTransition ( ( ) =>
97- setState ( { error : error as Error , isMutating : false } )
98- )
120+ startTransition ( ( ) => setState ( { error : error as Error } ) )
99121 options . onError ?.( error as Error , serializedKey , options )
100122 if ( options . throwOnError ) {
101123 throw error as Error
@@ -109,7 +131,7 @@ const mutation = (<Data, Error>() =>
109131
110132 const reset = useCallback ( ( ) => {
111133 ditchMutationsUntilRef . current = getTimestamp ( )
112- setState ( { data : UNDEFINED , error : UNDEFINED , isMutating : false } )
134+ setState ( { data : UNDEFINED , error : UNDEFINED } )
113135 // eslint-disable-next-line react-hooks/exhaustive-deps
114136 } , [ ] )
115137
@@ -133,10 +155,7 @@ const mutation = (<Data, Error>() =>
133155 stateDependencies . error = true
134156 return currentState . error
135157 } ,
136- get isMutating ( ) {
137- stateDependencies . isMutating = true
138- return currentState . isMutating
139- }
158+ isMutating
140159 }
141160 } ) as unknown as Middleware
142161
0 commit comments