import { act , screen , fireEvent } from '@testing-library/react' ; import React , { useEffect , useState } from 'react' ; import useSWR , { mutate as globalMutate , useSWRConfig } from 'swr' ; import { serialize } from '../src/utils/serialize' ; import { createResponse , sleep , nextTick , createKey , renderWithConfig , renderWithGlobalCache } from './utils' ; describe ( 'useSWR - local mutation' , ( ) => { it ( 'should trigger revalidation programmatically' , async ( ) => { let value = 0 , mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => value ++ , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; act ( ( ) => { mutate ( key ) ; } ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'should share local state when no fetcher is specified' , async ( ) => { const baseKey = createKey ( ) ; const useSharedState = ( key , fallbackData ) => { const { data : state , mutate : setState } = useSWR ( `${ baseKey }--${ key }` , { fallbackData } ) ; return [ state , setState ] ; } ; function Page ( ) { const [ name , setName ] = useSharedState ( 'name' , 'huozhi' ) ; const [ job , setJob ] = useSharedState ( 'job' , 'gardener' ) ; return ( < span onClick = { ( ) => { setName ( '@huozhi' ) ; setJob ( 'chef' ) ; } } > { name } : { job } < /span>); } renderWithConfig ( < Page />); const root = screen . getByText ( 'huozhi:gardener' ) ; fireEvent . click ( root ) ; await screen . findByText ( '@huozhi:chef' ) ; } ) ; it ( 'should trigger revalidation programmatically within a dedupingInterval' , async ( ) => { let value = 0 , mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => value ++ , { dedupingInterval : 2000 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; act ( ( ) => { mutate ( key ) ; } ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'should mutate the cache and revalidate' , async ( ) => { let value = 0 , mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => value ++ , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; act ( ( ) => { mutate ( key , 'mutate' ) ; } ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'should dedupe extra requests after mutation' , async ( ) => { let value = 0 , mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => value ++ , { dedupingInterval : 2000 } ) ; useSWR ( key , ( ) => value ++ , { dedupingInterval : 2000 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; act ( ( ) => { mutate ( key ) ; } ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'should mutate the cache and revalidate in async' , async ( ) => { let mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => createResponse ( 'truth' ) , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: truth' ) ; act ( ( ) => { mutate ( key , 'local' ) ; } ) ; await screen . findByText ( 'data: local' ) ; await screen . findByText ( 'data: truth' ) ; } ) ; it ( 'should support async mutation with promise' , async ( ) => { let mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => 0 , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; await nextTick ( ) ; await act ( ( ) => { return mutate ( key , createResponse ( 999 ) , false ) ; } ) ; await screen . findByText ( 'data: 999' ) ; } ) ; it ( 'should support async mutation with async function' , async ( ) => { let mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => 0 , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; await nextTick ( ) ; await act ( ( ) => { return mutate ( key , async ( ) => createResponse ( 999 ) , false ) ; } ) ; await screen . findByText ( 'data: 999' ) ; } ) ; it ( 'should trigger on mutation without data' , async ( ) => { let value = 0 , mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => value ++ , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; act ( ( ) => { mutate ( key ) ; } ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'should call function as data passing current cached value' , async ( ) => { let mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , null ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />, { provider : ( ) => new Map ( [ [ key , 'cached data' ] ] ) } ) ; const callback = jest . fn ( ) ; await act ( ( ) => mutate ( key , callback ) ) ; expect ( callback ) . toHaveBeenCalledWith ( 'cached data' ) ; } ) ; it ( 'should call function with undefined if key not cached' , async ( ) => { let cache , mutate ; function App ( ) { const { cache : cache_ , mutate : mutate_ } = useSWRConfig ( ) ; cache = cache_ ; mutate = mutate_ ; return null ; } renderWithConfig ( < App />); const increment = jest . fn ( currentValue => currentValue == null ? undefined : currentValue + 1 ) ; const key = createKey ( ) ; await mutate ( key , increment , false ) ; expect ( increment ) . toHaveBeenCalledTimes ( 1 ) ; expect ( increment ) . toHaveBeenLastCalledWith ( undefined ) ; expect ( increment ) . toHaveLastReturnedWith ( undefined ) ; cache . set ( key , 42 ) ; await mutate ( key , increment , false ) ; expect ( increment ) . toHaveBeenCalledTimes ( 2 ) ; expect ( increment ) . toHaveBeenLastCalledWith ( 42 ) ; expect ( increment ) . toHaveLastReturnedWith ( 43 ) ; } ) ; it ( 'should return results of the mutation' , async ( ) => { const key = createKey ( ) ; expect ( globalMutate ( key , Promise . resolve ( 'data' ) ) ) . resolves . toBe ( 'data' ) ; expect ( globalMutate ( key , Promise . reject ( new Error ( 'error' ) ) ) ) . rejects . toBeInstanceOf ( Error ) ; } ) ; it ( 'globalMutate should return undefined if the key is serialized to "" ' , async ( ) => { expect ( globalMutate ( null , Promise . resolve ( 'data' ) ) ) . resolves . toBe ( undefined ) ; expect ( globalMutate ( ( ) => { throw new Error ( 'error' ) ; } , Promise . resolve ( 'data' ) ) ) . resolves . toBe ( undefined ) ; } ) ; it ( 'should get bound mutate from useSWR' , async ( ) => { const key = createKey ( ) ; function Page ( ) { const { data , mutate : boundMutate } = useSWR ( key , ( ) => 'fetched' ) ; return ( < div onClick = { ( ) => boundMutate ( 'mutated' , false ) } > data : { data } < /div>); } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: fetched' ) ; fireEvent . click ( screen . getByText ( 'data: fetched' ) ) ; await screen . findByText ( 'data: mutated' ) ; } ) ; it ( 'should ignore in flight requests when mutating' , async ( ) => { const key = createKey ( ) ; act ( ( ) => { globalMutate ( key , 1 ) ; } ) ; function Section ( ) { const { data } = useSWR ( key , ( ) => createResponse ( 2 , { delay : 150 } ) ) ; return < div > { data } < /div>; } renderWithGlobalCache ( < Section />); screen . getByText ( '1' ) ; await act ( ( ) => sleep ( 100 ) ) ; act ( ( ) => { globalMutate ( key , 3 ) ; } ) ; await screen . findByText ( '3' ) ; await act ( ( ) => sleep ( 100 ) ) ; screen . getByText ( '3' ) ; } ) ; it ( 'should ignore in flight mutations when calling another async mutate' , async ( ) => { let value = 'off' , mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => createResponse ( value , { delay : 100 } ) ) ; return < div > { data } < /div>; } renderWithConfig ( < Page />); await screen . findByText ( 'off' ) ; act ( ( ) => { mutate ( key , 'on' , false ) ; } ) ; await screen . findByText ( 'on' ) ; await act ( async ( ) => { expect ( mutate ( key , ( ) => { value = 'on' ; return createResponse ( 'on' , { delay : 100 } ) ; } , false ) ) . resolves . toBe ( 'on' ) ; } ) ; act ( ( ) => { mutate ( key , 'off' , false ) ; } ) ; await screen . findByText ( 'off' ) ; await act ( async ( ) => { expect ( mutate ( key , ( ) => { value = 'off' ; return createResponse ( 'off' , { delay : 100 } ) ; } , false ) ) . resolves . toBe ( 'off' ) ; } ) ; await act ( ( ) => sleep ( 50 ) ) ; screen . getByText ( 'off' ) ; await act ( ( ) => sleep ( 100 ) ) ; screen . getByText ( 'off' ) ; } ) ; it ( 'null is stringified when found inside an array' , async ( ) => { let value = 0 , mutate ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( [ null ] , ( ) => value ++ , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; act ( ( ) => { mutate ( [ null ] ) ; } ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'should return promise from mutate without data' , async ( ) => { let value = 0 , mutate ; const key = createKey ( ) ; function Page ( ) { mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => value ++ , { dedupingInterval : 0 } ) ; return < div > data : { data } < /div>; } renderWithConfig ( < Page />); screen . getByText ( 'data:' ) ; await screen . findByText ( 'data: 0' ) ; let promise ; await act ( ( ) => { promise = mutate ( key ) ; return promise ; } ) ; expect ( promise ) . toBeInstanceOf ( Promise ) ; expect ( promise ) . resolves . toBe ( 1 ) ; screen . getByText ( 'data: 1' ) ; } ) ; it ( 'should update error in cache when mutate failed with error' , async ( ) => { const value = 0 ; const key = createKey ( ) ; const message = 'mutate-error' ; let cache , mutate ; function Page ( ) { const { cache : cache_ , mutate : mutate_ } = useSWRConfig ( ) ; cache = cache_ ; mutate = mutate_ ; const { data , error } = useSWR ( key , ( ) => value ) ; return < div > { error ? error . message : `data: ${ data }` } < /div>; } renderWithConfig ( < Page />); await screen . findByText ( 'data: 0' ) ; await act ( async ( ) => { try { await mutate ( key , ( ) => { throw new Error ( message ) ; } , false ) ; } catch ( e ) { } } ) ; screen . getByText ( message ) ; const [ keyData , , keyInfo ] = serialize ( key ) ; let cacheError = cache . get ( keyInfo ) ?. error ; expect ( cacheError . message ) . toMatchInlineSnapshot ( `"${ message }"` ) ; expect ( cache . get ( keyData ) ) . toBe ( value ) ; await act ( ( ) => mutate ( key , value , false ) ) ; cacheError = cache . get ( keyInfo ) ?. error ; expect ( cacheError ) . toMatchInlineSnapshot ( `undefined` ) ; } ) ; it ( 'should keep the `mutate` function referential equal' , async ( ) => { const refs = [ ] ; let mutate ; const updatedKey = createKey ( ) ; function Section ( ) { mutate = useSWRConfig ( ) . mutate ; const [ key , setKey ] = useState ( null ) ; const { data , mutate : boundMutate } = useSWR ( key , ( ) => createResponse ( 1 ) ) ; useEffect ( ( ) => { const timeout = setTimeout ( ( ) => setKey ( updatedKey ) , 50 ) ; return ( ) => clearTimeout ( timeout ) ; } , [ ] ) ; refs . push ( boundMutate ) ; return < div > { data } < /div>; } renderWithConfig ( < Section />); await act ( ( ) => sleep ( 100 ) ) ; act ( ( ) => { mutate ( updatedKey , 2 ) ; } ) ; await act ( ( ) => sleep ( 50 ) ) ; for ( const ref of refs ) { expect ( ref ) . toEqual ( refs [ 0 ] ) ; } } ) ; it ( 'should not dedupe synchronous mutations' , async ( ) => { const mutationRecivedValues = [ ] ; const renderRecivedValues = [ ] ; const key = createKey ( ) ; function Component ( ) { const { data , mutate : boundMutate } = useSWR ( key , ( ) => 0 ) ; useEffect ( ( ) => { setTimeout ( ( ) => { boundMutate ( v => { mutationRecivedValues . push ( v ) ; return 1 ; } , false ) ; boundMutate ( v => { mutationRecivedValues . push ( v ) ; return 2 ; } , false ) ; } , 1 ) ; } , [ ] ) ; renderRecivedValues . push ( data ) ; return null ; } renderWithConfig ( < Component />); await act ( ( ) => sleep ( 50 ) ) ; expect ( mutationRecivedValues ) . toEqual ( [ 0 , 1 ] ) ; expect ( renderRecivedValues ) . toEqual ( [ undefined , 0 , 1 , 2 ] ) ; } ) ; it ( 'async mutation case 1 (startAt <= MUTATION_TS[key])' , async ( ) => { let result = 0 ; const key = createKey ( ) ; const fetcher = jest . fn ( createResponse ) ; function Component ( ) { const { data , mutate : boundMutate } = useSWR ( key , ( ) => fetcher ( 0 , { delay : 300 } ) , { dedupingInterval : 200 } ) ; return ( < div onClick = { ( ) => { boundMutate ( async ( ) => { result += 1 ; return createResponse ( result , { delay : 100 } ) ; } , false ) ; } } > { data !== undefined ? `data: ${ data . toString ( ) }` : 'loading' } < /div>); } renderWithConfig ( < Component />); screen . getByText ( 'loading' ) ; await act ( ( ) => sleep ( 50 ) ) ; fireEvent . click ( screen . getByText ( 'loading' ) ) ; await act ( ( ) => sleep ( 100 ) ) ; await screen . findByText ( 'data: 1' ) ; await act ( ( ) => sleep ( 150 ) ) ; expect ( fetcher ) . toBeCalledTimes ( 1 ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'async mutation case 2 (startAt <= MUTATION_END_TS[key])' , async ( ) => { let result = 0 , mutate ; const fetcher = jest . fn ( createResponse ) ; const updatedKey = createKey ( ) ; function Component ( ) { const [ key , setKey ] = useState ( null ) ; mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => fetcher ( 0 , { delay : 400 } ) , { dedupingInterval : 200 } ) ; useEffect ( ( ) => { mutate ( updatedKey , async ( ) => { result += 1 ; return createResponse ( result , { delay : 200 } ) ; } , false ) ; setKey ( updatedKey ) ; } , [ ] ) ; return ( < div > { data !== undefined ? `data: ${ data . toString ( ) }` : 'loading' } < /div>); } renderWithConfig ( < Component />); screen . getByText ( 'loading' ) ; await act ( ( ) => sleep ( 200 ) ) ; fireEvent . click ( screen . getByText ( 'data: 1' ) ) ; await act ( ( ) => sleep ( 200 ) ) ; expect ( fetcher ) . toBeCalledTimes ( 1 ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'async mutation case 3 (MUTATION_END_TS[key] === 0)' , async ( ) => { let result = 0 , mutate ; const fetcher = jest . fn ( createResponse ) ; const updatedKey = createKey ( ) ; function Component ( ) { const [ key , setKey ] = useState ( null ) ; mutate = useSWRConfig ( ) . mutate ; const { data } = useSWR ( key , ( ) => fetcher ( 0 , { delay : 100 } ) , { dedupingInterval : 200 } ) ; useEffect ( ( ) => { setKey ( updatedKey ) ; mutate ( updatedKey , async ( ) => { result += 1 ; return createResponse ( result , { delay : 200 } ) ; } , false ) ; } , [ ] ) ; return ( < div > { data !== undefined ? `data: ${ data . toString ( ) }` : 'loading' } < /div>); } renderWithConfig ( < Component />); screen . getByText ( 'loading' ) ; await act ( ( ) => sleep ( 100 ) ) ; expect ( fetcher ) . toBeCalledTimes ( 1 ) ; screen . getByText ( 'loading' ) ; await act ( ( ) => sleep ( 100 ) ) ; await screen . findByText ( 'data: 1' ) ; } ) ; it ( 'isValidating should be false when no fetcher is provided' , async ( ) => { const key = createKey ( ) ; function Page ( ) { const { isValidating } = useSWR ( key ) ; return < p > { isValidating . toString ( ) } < /p>; } renderWithConfig ( < Page />); screen . getByText ( 'false' ) ; } ) ; it ( 'bound mutate should always use the latest key' , async ( ) => { const key = createKey ( ) ; const fetcher = jest . fn ( ( ) => 'data' ) ; function Page ( ) { const [ ready , setReady ] = useState ( false ) ; const { mutate : boundMutate } = useSWR ( ready ? key : null , fetcher ) ; return ( < div > < button onClick = { ( ) => setReady ( true ) } > set ready < /button> < button onClick = { ( ) => boundMutate ( ) } > mutate < /button> < /div>); } renderWithConfig ( < Page />); screen . getByText ( 'set ready' ) ; expect ( fetcher ) . toBeCalledTimes ( 0 ) ; fireEvent . click ( screen . getByText ( 'set ready' ) ) ; await act ( ( ) => sleep ( 10 ) ) ; expect ( fetcher ) . toBeCalledTimes ( 1 ) ; fireEvent . click ( screen . getByText ( 'mutate' ) ) ; await act ( ( ) => sleep ( 10 ) ) ; expect ( fetcher ) . toBeCalledTimes ( 2 ) ; } ) ; it ( 'should reset isValidating after mutate' , async ( ) => { const key = createKey ( ) ; function Data ( ) { const { data , isValidating } = useSWR ( key , ( ) => createResponse ( 'data' , { delay : 30 } ) ) ; const { cache } = useSWRConfig ( ) ; const [ , , keyInfo ] = serialize ( key ) ; const cacheIsValidating = cache . get ( keyInfo ) ?. isValidating ; return ( < > < p > data : { data } < /p> < p > isValidating : { isValidating . toString ( ) } < /p> < p > cache : validating : { cacheIsValidating . toString ( ) } < /p> < />); } function Page ( ) { const { mutate : boundMutate } = useSWR ( key , ( ) => createResponse ( 'data' , { delay : 30 } ) ) ; const [ visible , setVisible ] = useState ( false ) ; return ( < div > < button onClick = { ( ) => boundMutate ( ( ) => 'data' , false ) } > preload < /button> < button onClick = { ( ) => setVisible ( true ) } > show < /button> { visible && < Data />} < /div>); } renderWithConfig ( < Page />); fireEvent . click ( screen . getByText ( 'preload' ) ) ; await act ( ( ) => sleep ( 20 ) ) ; fireEvent . click ( screen . getByText ( 'show' ) ) ; screen . getByText ( 'data:data' ) ; screen . getByText ( 'isValidating:true' ) ; await act ( ( ) => sleep ( 20 ) ) ; screen . getByText ( 'data:data' ) ; screen . getByText ( 'isValidating:false' ) ; } ) ; it ( 'should be able to mutate data to undefined' , async ( ) => { const key = createKey ( ) ; function Page ( ) { const { data , mutate } = useSWR ( key , ( ) => 'foo' ) ; return ( < > < div > data : { String ( data ) } < /div> < button onClick = { ( ) => mutate ( undefined , false ) } > mutate < /button> < />); } renderWithConfig ( < Page />); await screen . findByText ( 'data: foo' ) ; fireEvent . click ( screen . getByText ( 'mutate' ) ) ; await screen . findByText ( 'data: undefined' ) ; } ) ; it ( 'should be able to mutate data to undefined asynchronously' , async ( ) => { const key = createKey ( ) ; function Page ( ) { const { data , mutate } = useSWR ( key , ( ) => 'foo' ) ; return ( < > < div > data : { String ( data ) } < /div> < button onClick = { ( ) => mutate ( ( ) => new Promise ( res => setTimeout ( ( ) => res ( undefined ) , 10 ) ) , false ) } > mutate < /button> < />); } renderWithConfig ( < Page />); await screen . findByText ( 'data: foo' ) ; fireEvent . click ( screen . getByText ( 'mutate' ) ) ; await screen . findByText ( 'data: undefined' ) ; } ) ; it ( 'should be able to deduplicate multiple mutate calls' , async ( ) => { const key = createKey ( ) ; const loggedData = [ ] ; function Page ( ) { const { data , mutate } = useSWR ( key , ( ) => 'foo' ) ; useEffect ( ( ) => { async function startMutation ( ) { await sleep ( 10 ) ; mutate ( 'sync1' , false ) ; mutate ( createResponse ( 'async1' , { delay : 50 } ) , false ) ; await sleep ( 10 ) ; mutate ( 'sync2' , false ) ; mutate ( createResponse ( 'async2' , { delay : 50 } ) , false ) ; await sleep ( 10 ) ; mutate ( 'sync3' , false ) ; mutate ( createResponse ( 'async3' , { delay : 50 } ) , false ) ; } startMutation ( ) ; } , [ mutate ] ) ; loggedData . push ( data ) ; return null ; } renderWithConfig ( < Page />); await act ( ( ) => sleep ( 200 ) ) ; expect ( loggedData ) . toEqual ( [ undefined , 'foo' , 'sync1' , 'sync2' , 'sync3' , 'async3' ] ) ; } ) ; it ( 'should ignore in flight mutation error when calling another async mutate' , async ( ) => { const key = createKey ( ) ; const errorMutate = ( ) => new Promise < string > ( ( _ , reject ) => { setTimeout ( ( ) => reject ( 'error' ) , 200 ) ; } ) ; const successMutate = ( ) => new Promise < string > ( resolve => { setTimeout ( ( ) => resolve ( 'success' ) , 100 ) ; } ) ; function Page ( ) { const { data , mutate : boundMutate } = useSWR ( key , ( ) => createResponse ( 'data' , { delay : 100 } ) ) ; return ( < div > < div > { data } < /div> < button onClick = { ( ) => { boundMutate ( successMutate , false ) ; } } > success - mutate < /button> < button onClick = { ( ) => { boundMutate ( errorMutate , false ) . catch ( ( ) => { } ) ; } } > error - mutate < /button> < /div>); } renderWithConfig ( < Page />); await screen . findByText ( 'data' ) ; fireEvent . click ( screen . getByText ( 'error-mutate' ) ) ; await sleep ( 50 ) ; fireEvent . click ( screen . getByText ( 'success-mutate' ) ) ; await screen . findByText ( 'success' ) ; await sleep ( 300 ) ; await screen . findByText ( 'success' ) ; } ) ; it ( 'should not update the cache when `populateCache` is disabled' , async ( ) => { const key = createKey ( ) ; function Page ( ) { const { data , mutate } = useSWR ( key , ( ) => 'foo' ) ; return ( < > < div > data : { String ( data ) } < /div> < button onClick = { ( ) => mutate ( 'bar' , { revalidate : false , populateCache : false } ) } > mutate < /button> < />); } renderWithConfig ( < Page />); await screen . findByText ( 'data: foo' ) ; fireEvent . click ( screen . getByText ( 'mutate' ) ) ; await sleep ( 30 ) ; await screen . findByText ( 'data: foo' ) ; } ) ; it ( 'should support optimistic updates via `optimisticData`' , async ( ) => { const key = createKey ( ) ; const renderedData = [ ] ; let mutate ; function Page ( ) { const { data , mutate : boundMutate } = useSWR ( key , ( ) => createResponse ( 'foo' , { delay : 20 } ) ) ; mutate = boundMutate ; renderedData . push ( data ) ; return < div > data : { String ( data ) } < /div>; } renderWithConfig ( < Page />); await screen . findByText ( 'data: foo' ) ; await act ( ( ) => mutate ( createResponse ( 'baz' , { delay : 20 } ) , { optimisticData : 'bar' } ) ) ; await sleep ( 30 ) ; expect ( renderedData ) . toEqual ( [ undefined , 'foo' , 'bar' , 'baz' , 'foo' ] ) ; } ) ; it ( 'should rollback optimistic updates when mutation fails' , async ( ) => { const key = createKey ( ) ; const renderedData = [ ] ; let mutate ; let cnt = 0 ; function Page ( ) { const { data , mutate : boundMutate } = useSWR ( key , ( ) => createResponse ( cnt ++ , { delay : 20 } ) ) ; mutate = boundMutate ; if ( ! renderedData . length || renderedData [ renderedData . length - 1 ] !== data ) { renderedData . push ( data ) ; } return < div > data : { String ( data ) } < /div>; } renderWithConfig ( < Page />); await screen . findByText ( 'data: 0' ) ; try { await act ( ( ) => mutate ( createResponse ( new Error ( 'baz' ) , { delay : 20 } ) , { optimisticData : 'bar' } ) ) ; } catch ( e ) { expect ( e . message ) . toEqual ( 'baz' ) ; } await sleep ( 30 ) ; expect ( renderedData ) . toEqual ( [ undefined , 0 , 'bar' , 0 , 1 ] ) ; } ) ; it ( 'should not rollback optimistic updates if `rollbackOnError`' , async ( ) => { const key = createKey ( ) ; const renderedData = [ ] ; let mutate ; let cnt = 0 ; function Page ( ) { const { data , mutate : boundMutate } = useSWR ( key , ( ) => createResponse ( cnt ++ , { delay : 20 } ) ) ; mutate = boundMutate ; if ( ! renderedData . length || renderedData [ renderedData . length - 1 ] !== data ) { renderedData . push ( data ) ; } return < div > data : { String ( data ) } < /div>; } renderWithConfig ( < Page />); await screen . findByText ( 'data: 0' ) ; try { await act ( ( ) => mutate ( createResponse ( new Error ( 'baz' ) , { delay : 20 } ) , { optimisticData : 'bar' , rollbackOnError : false } ) ) ; } catch ( e ) { expect ( e . message ) . toEqual ( 'baz' ) ; } await sleep ( 30 ) ; expect ( renderedData ) . toEqual ( [ undefined , 0 , 'bar' , 1 ] ) ; } ) ; } ) ;