Skip to content

Commit d7643b5

Browse files
authored
test({react,preact}-query/useMutation): add optimistic update tests with success and rollback on error (TanStack#10492)
1 parent cd89d6f commit d7643b5

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

packages/preact-query/src/__tests__/useMutation.test.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,4 +2088,123 @@ describe('useMutation', () => {
20882088

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

packages/react-query/src/__tests__/useMutation.test.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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: /delete item2/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: /delete item2/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

Comments
 (0)