Skip to content

Commit c3e3c2d

Browse files
authored
feat: use-request support rollbackError (#156)
* test: use-request rollbackError * feat: use-request support rollbackError * docs: use-request rollbackError
1 parent 64f8bd9 commit c3e3c2d

File tree

5 files changed

+58
-18
lines changed

5 files changed

+58
-18
lines changed

packages/hooks/src/useRequest/Fetch.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Ref } from 'vue'
2+
import { isFunction, isBoolean } from '../utils'
23
import {
34
UseRequestFetchState,
45
UseRequestOptions,
@@ -18,6 +19,8 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
1819
error: undefined,
1920
}
2021

22+
previousValidData: UseRequestFetchState<TData, TParams>['data'] = undefined
23+
2124
constructor(
2225
public serviceRef: Ref<UseRequestService<TData, TParams>>,
2326
public options: UseRequestOptions<TData, TParams, any>,
@@ -34,7 +37,10 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
3437
}
3538
}
3639

37-
// 设置state
40+
/**
41+
* set state
42+
* @param currentState currentState
43+
*/
3844
setState(currentState: Partial<UseRequestFetchState<TData, TParams>> = {}) {
3945
this.state = {
4046
...this.state,
@@ -89,7 +95,7 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
8995
}
9096

9197
/**
92-
* Traverse the plugin that needs to be run,
98+
* Traverse the plugin that needs to be run,
9399
* which is a callback function for the plugin to obtain fetch instances and execute plugin logic at the corresponding nodes.
94100
*/
95101
runPluginHandler(event: keyof UseRequestPluginReturn<TData, TParams>, ...rest: unknown[]) {
@@ -110,7 +116,7 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
110116
)
111117
// Do you want to stop the request
112118
if (stopNow) {
113-
return new Promise(() => { })
119+
return new Promise(() => {})
114120
}
115121

116122
this.setState({
@@ -134,7 +140,7 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
134140
const requestReturnResponse = (res: any) => {
135141
// The request has been cancelled, and the count will be inconsistent with the currentCount
136142
if (currentCount !== this.count) {
137-
return new Promise(() => { })
143+
return new Promise(() => {})
138144
}
139145
// Format data
140146
const formattedResult = this.options.formatResult ? this.options.formatResult(res) : res
@@ -149,6 +155,8 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
149155

150156
this.runPluginHandler('onSuccess', formattedResult, params)
151157

158+
this.previousValidData = formattedResult
159+
152160
// Execute whether the request is successful or unsuccessful
153161
this.options.onFinally?.(params, formattedResult, undefined)
154162

@@ -166,7 +174,7 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
166174
return requestReturnResponse(servicePromiseResult)
167175
} catch (error) {
168176
if (currentCount !== this.count) {
169-
return new Promise(() => { })
177+
return new Promise(() => {})
170178
}
171179

172180
this.setState({
@@ -177,6 +185,16 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
177185
this.options.onError?.(error as Error, params)
178186
this.runPluginHandler('onError', error, params)
179187

188+
// rollback
189+
if (
190+
(isFunction(this.options?.rollbackOnError) && this.options?.rollbackOnError(params)) ||
191+
(isBoolean(this.options?.rollbackOnError) && this.options.rollbackOnError)
192+
) {
193+
this.setState({
194+
data: this.previousValidData,
195+
})
196+
}
197+
180198
// Execute whether the request is successful or unsuccessful
181199
this.options.onFinally?.(params, undefined, error as Error)
182200

@@ -214,14 +232,7 @@ export default class Fetch<TData, TParams extends unknown[] = any> {
214232
}
215233

216234
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
217-
let targetData: TData | undefined
218-
if (typeof data === 'function') {
219-
// @ts-ignore
220-
targetData = data?.(this.state.data)
221-
} else {
222-
targetData = data
223-
}
224-
235+
const targetData = isFunction(data) ? data(this.state.data) : data
225236
this.runPluginHandler('onMutate', targetData)
226237
this.setState({
227238
data: targetData,

packages/hooks/src/useRequest/__tests__/basic.spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { sleep } from 'test-utils/sleep'
33
import useRequest from '../useRequest'
44

55
function getUsername(params: { desc: string }): Promise<string> {
6-
return new Promise(resolve => {
6+
return new Promise((resolve, reject) => {
77
setTimeout(() => {
8-
resolve(`vue-hooks-plus ${params.desc}`)
8+
if (params?.desc && params.desc === 'nice2') reject('error')
9+
resolve(`vue-hooks-plus ${params?.desc}`)
910
}, 200)
1011
})
1112
}
@@ -60,4 +61,20 @@ describe('useRequest/Basic', () => {
6061
await sleep(200)
6162
expect(hook.params.value[0]?.desc).toBe('nice1')
6263
})
64+
65+
it('should mutate and rollbackError', async () => {
66+
const [hook] = renderHook(() =>
67+
useRequest(getUsername, {
68+
rollbackOnError: true,
69+
}),
70+
)
71+
hook.mutate('vue-hooks-plus nice')
72+
expect(hook.data.value).toBe('vue-hooks-plus nice')
73+
hook.run({ desc: 'nice' })
74+
await sleep(200)
75+
expect(hook.data.value).toBe('vue-hooks-plus nice')
76+
hook.run({ desc: 'nice2' })
77+
await sleep(200)
78+
expect(hook.data.value).toBe('vue-hooks-plus nice')
79+
})
6380
})

packages/hooks/src/useRequest/docs/basic/demo/demo5.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div>name:{{ data }}</div>
33
<div style="margin-top:8px">
4-
<input v-model="value">
4+
<input v-model="value" />
55
<vhp-button style="margin-left: 8px;" @click="handleClick">Edit</vhp-button>
66
</div>
77
<div style="margin-top:8px">
@@ -25,12 +25,12 @@
2525
const step = ref<string[]>([])
2626
const { data: data, run, mutate } = useRequest(getUsername, {
2727
manual: true,
28-
devKey:"demo5",
28+
devKey: 'demo5',
29+
rollbackOnError: true,
2930
onError: () => {
3031
alert('error')
3132
},
3233
})
33-
3434
const handleClick = () => {
3535
mutate(value.value)
3636
run({ desc: value.value })

packages/hooks/src/useRequest/docs/basic/index.en-US.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ Of course, the difference between `refresh` and `refreshAsync` is the same as `r
103103

104104
## Change data immediately
105105

106+
### optimisticData
107+
106108
`useRequest` provides `mutate`, which can immediate modify the `data`.
107109

108110
The usage of `mutate` is consistent with `React.setState`, supports: `mutate(newData)` and `mutate((oldData) => newData)`.
@@ -111,6 +113,10 @@ In the following example, we demonstrate a scenario of `mutate`.
111113

112114
We have modified the user name, but we do not want to wait for the request to be successful before giving feedback to the user. Instead, modify the data directly, then call the modify request in background, and provide additional feedback after the request returns.
113115

116+
### error rollback
117+
118+
When you use `mutate`, it is possible that the remote data change fails after the optimistic data is displayed to the user. In this case, you can enable `rollbackOnError`, which restores the local cache to its previous state, ensuring that the user sees Got the correct data.
119+
114120
<demo src="./demo/demo5.vue"
115121
language="vue"
116122
title=""

packages/hooks/src/useRequest/docs/basic/index.zh-CN.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ runAsync()
100100

101101
## 立即变更数据
102102

103+
### 乐观更新
104+
103105
`useRequest` 提供了 `mutate`, 支持立即修改 `useRequest` 返回的 `data` 参数。
104106

105107
支持 `mutate(newData)``mutate((oldData) => newData)` 两种写法。
@@ -108,6 +110,10 @@ runAsync()
108110

109111
我们修改了用户名,但是我们不希望等编辑接口调用成功之后,才给用户反馈。而是直接修改页面数据,同时在背后去调用修改接口,等修改接口返回之后,另外提供反馈。
110112

113+
### 错误回滚
114+
115+
当你使用 `mutate`时,有可能在乐观数据展示给用户后,远程数据更改却失败了。在这种情况下,你可以启用 `rollbackOnError`,将本地缓存恢复到之前的状态,确保用户看到的是正确的数据。
116+
111117
<demo src="./demo/demo5.vue"
112118
language="vue"
113119
title=""

0 commit comments

Comments
 (0)