@@ -2087,4 +2087,123 @@ describe('useMutation', () => {
20872087
20882088 expect ( rendered . getByText ( 'message: result: success' ) ) . toBeInTheDocument ( )
20892089 } )
2090+
2091+ it ( 'should support optimistic update on success' , async ( ) => {
2092+ function Page ( ) {
2093+ const [ items , setItems ] = React . useState < Array < string > > ( [
2094+ 'item1' ,
2095+ 'item2' ,
2096+ 'item3' ,
2097+ ] )
2098+
2099+ const [ successMessage , setSuccessMessage ] = React . useState < string > ( '' )
2100+
2101+ const deleteMutation = useMutation ( {
2102+ mutationFn : ( item : string ) => sleep ( 10 ) . then ( ( ) => item ) ,
2103+ onMutate : ( item ) => {
2104+ const previousItems = [ ...items ]
2105+ setItems ( ( prev ) => prev . filter ( ( i ) => i !== item ) )
2106+ return { previousItems }
2107+ } ,
2108+ onSuccess : ( deletedItem ) => {
2109+ setSuccessMessage ( `deleted: ${ deletedItem } ` )
2110+ } ,
2111+ onError : ( _error , _item , context ) => {
2112+ if ( context ?. previousItems ) {
2113+ setItems ( context . previousItems )
2114+ }
2115+ } ,
2116+ } )
2117+
2118+ return (
2119+ < div >
2120+ { items . map ( ( item ) => (
2121+ < button key = { item } onClick = { ( ) => deleteMutation . mutate ( item ) } >
2122+ delete { item }
2123+ </ button >
2124+ ) ) }
2125+ < div > items: { items . join ( ', ' ) } </ div >
2126+ < div > success: { successMessage || 'none' } </ div >
2127+ </ div >
2128+ )
2129+ }
2130+
2131+ const rendered = renderWithClient ( queryClient , < Page /> )
2132+
2133+ expect ( rendered . getByText ( 'items: item1, item2, item3' ) ) . toBeInTheDocument ( )
2134+ expect ( rendered . getByText ( 'success: none' ) ) . toBeInTheDocument ( )
2135+
2136+ fireEvent . click ( rendered . getByRole ( 'button' , { name : / d e l e t e i t e m 2 / i } ) )
2137+
2138+ // optimistic update: item2 removed immediately
2139+ expect ( rendered . getByText ( 'items: item1, item3' ) ) . toBeInTheDocument ( )
2140+
2141+ await vi . advanceTimersByTimeAsync ( 11 )
2142+
2143+ // success: item2 stays removed and onSuccess called
2144+ expect ( rendered . getByText ( 'items: item1, item3' ) ) . toBeInTheDocument ( )
2145+ expect ( rendered . getByText ( 'success: deleted: item2' ) ) . toBeInTheDocument ( )
2146+ } )
2147+
2148+ it ( 'should support optimistic update and rollback on error' , async ( ) => {
2149+ function Page ( ) {
2150+ const [ items , setItems ] = React . useState < Array < string > > ( [
2151+ 'item1' ,
2152+ 'item2' ,
2153+ 'item3' ,
2154+ ] )
2155+
2156+ const [ message , setMessage ] = React . useState < string > ( '' )
2157+
2158+ const deleteMutation = useMutation ( {
2159+ mutationFn : ( item : string ) =>
2160+ sleep ( 10 ) . then ( ( ) => {
2161+ throw new Error ( `Failed to delete ${ item } ` )
2162+ } ) ,
2163+ onMutate : ( item ) => {
2164+ const previousItems = [ ...items ]
2165+ setItems ( ( prev ) => prev . filter ( ( i ) => i !== item ) )
2166+ return { previousItems }
2167+ } ,
2168+ onSuccess : ( deletedItem ) => {
2169+ setMessage ( `deleted: ${ deletedItem } ` )
2170+ } ,
2171+ onError : ( _error , _item , context ) => {
2172+ setMessage ( 'rollback' )
2173+ if ( context ?. previousItems ) {
2174+ setItems ( context . previousItems )
2175+ }
2176+ } ,
2177+ retry : false ,
2178+ } )
2179+
2180+ return (
2181+ < div >
2182+ { items . map ( ( item ) => (
2183+ < button key = { item } onClick = { ( ) => deleteMutation . mutate ( item ) } >
2184+ delete { item }
2185+ </ button >
2186+ ) ) }
2187+ < div > items: { items . join ( ', ' ) } </ div >
2188+ < div > message: { message || 'none' } </ div >
2189+ </ div >
2190+ )
2191+ }
2192+
2193+ const rendered = renderWithClient ( queryClient , < Page /> )
2194+
2195+ expect ( rendered . getByText ( 'items: item1, item2, item3' ) ) . toBeInTheDocument ( )
2196+ expect ( rendered . getByText ( 'message: none' ) ) . toBeInTheDocument ( )
2197+
2198+ fireEvent . click ( rendered . getByRole ( 'button' , { name : / d e l e t e i t e m 2 / i } ) )
2199+
2200+ // optimistic update: item2 removed immediately
2201+ expect ( rendered . getByText ( 'items: item1, item3' ) ) . toBeInTheDocument ( )
2202+
2203+ await vi . advanceTimersByTimeAsync ( 11 )
2204+
2205+ // rollback: item2 restored after error, onSuccess not called
2206+ expect ( rendered . getByText ( 'items: item1, item2, item3' ) ) . toBeInTheDocument ( )
2207+ expect ( rendered . getByText ( 'message: rollback' ) ) . toBeInTheDocument ( )
2208+ } )
20902209} )
0 commit comments