@ahoo-wang/fetcher-react 3.5.6 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +869 -483
- package/README.zh-CN.md +2139 -605
- package/dist/core/debounced/index.d.ts +4 -0
- package/dist/core/debounced/index.d.ts.map +1 -0
- package/dist/core/debounced/useDebouncedCallback.d.ts.map +1 -0
- package/dist/core/{useDebouncedExecutePromise.d.ts → debounced/useDebouncedExecutePromise.d.ts} +1 -1
- package/dist/core/debounced/useDebouncedExecutePromise.d.ts.map +1 -0
- package/dist/{wow/debounce → core/debounced}/useDebouncedQuery.d.ts +1 -2
- package/dist/core/debounced/useDebouncedQuery.d.ts.map +1 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/{wow → core}/useQuery.d.ts +2 -2
- package/dist/core/useQuery.d.ts.map +1 -0
- package/dist/{wow → core}/useQueryState.d.ts +1 -1
- package/dist/core/useQueryState.d.ts.map +1 -0
- package/dist/cosec/RouteGuard.d.ts +49 -0
- package/dist/cosec/RouteGuard.d.ts.map +1 -0
- package/dist/cosec/SecurityContext.d.ts +47 -14
- package/dist/cosec/SecurityContext.d.ts.map +1 -1
- package/dist/cosec/index.d.ts +1 -0
- package/dist/cosec/index.d.ts.map +1 -1
- package/dist/cosec/useSecurity.d.ts +60 -8
- package/dist/cosec/useSecurity.d.ts.map +1 -1
- package/dist/fetcher/debounced/index.d.ts +3 -0
- package/dist/fetcher/debounced/index.d.ts.map +1 -0
- package/dist/fetcher/{useDebouncedFetcher.d.ts → debounced/useDebouncedFetcher.d.ts} +2 -2
- package/dist/fetcher/debounced/useDebouncedFetcher.d.ts.map +1 -0
- package/dist/{wow/debounce → fetcher/debounced}/useDebouncedFetcherQuery.d.ts +1 -1
- package/dist/fetcher/debounced/useDebouncedFetcherQuery.d.ts.map +1 -0
- package/dist/fetcher/index.d.ts +2 -1
- package/dist/fetcher/index.d.ts.map +1 -1
- package/dist/{wow → fetcher}/useFetcherQuery.d.ts +3 -3
- package/dist/fetcher/useFetcherQuery.d.ts.map +1 -0
- package/dist/index.es.js +567 -538
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wow/fetcher/index.d.ts +6 -0
- package/dist/wow/fetcher/index.d.ts.map +1 -0
- package/dist/wow/{useFetcherCountQuery.d.ts → fetcher/useFetcherCountQuery.d.ts} +2 -2
- package/dist/wow/fetcher/useFetcherCountQuery.d.ts.map +1 -0
- package/dist/wow/{useFetcherListQuery.d.ts → fetcher/useFetcherListQuery.d.ts} +2 -2
- package/dist/wow/fetcher/useFetcherListQuery.d.ts.map +1 -0
- package/dist/wow/{useFetcherListStreamQuery.d.ts → fetcher/useFetcherListStreamQuery.d.ts} +2 -2
- package/dist/wow/fetcher/useFetcherListStreamQuery.d.ts.map +1 -0
- package/dist/wow/{useFetcherPagedQuery.d.ts → fetcher/useFetcherPagedQuery.d.ts} +2 -2
- package/dist/wow/fetcher/useFetcherPagedQuery.d.ts.map +1 -0
- package/dist/wow/{useFetcherSingleQuery.d.ts → fetcher/useFetcherSingleQuery.d.ts} +2 -2
- package/dist/wow/fetcher/useFetcherSingleQuery.d.ts.map +1 -0
- package/dist/wow/index.d.ts +1 -10
- package/dist/wow/index.d.ts.map +1 -1
- package/dist/wow/useCountQuery.d.ts +1 -1
- package/dist/wow/useCountQuery.d.ts.map +1 -1
- package/dist/wow/useListQuery.d.ts +1 -1
- package/dist/wow/useListQuery.d.ts.map +1 -1
- package/dist/wow/useListStreamQuery.d.ts +1 -1
- package/dist/wow/useListStreamQuery.d.ts.map +1 -1
- package/dist/wow/usePagedQuery.d.ts +1 -1
- package/dist/wow/usePagedQuery.d.ts.map +1 -1
- package/dist/wow/useSingleQuery.d.ts +1 -1
- package/dist/wow/useSingleQuery.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/core/useDebouncedCallback.d.ts.map +0 -1
- package/dist/core/useDebouncedExecutePromise.d.ts.map +0 -1
- package/dist/fetcher/useDebouncedFetcher.d.ts.map +0 -1
- package/dist/wow/debounce/index.d.ts +0 -3
- package/dist/wow/debounce/index.d.ts.map +0 -1
- package/dist/wow/debounce/useDebouncedFetcherQuery.d.ts.map +0 -1
- package/dist/wow/debounce/useDebouncedQuery.d.ts.map +0 -1
- package/dist/wow/types.d.ts +0 -7
- package/dist/wow/types.d.ts.map +0 -1
- package/dist/wow/useFetcherCountQuery.d.ts.map +0 -1
- package/dist/wow/useFetcherListQuery.d.ts.map +0 -1
- package/dist/wow/useFetcherListStreamQuery.d.ts.map +0 -1
- package/dist/wow/useFetcherPagedQuery.d.ts.map +0 -1
- package/dist/wow/useFetcherQuery.d.ts.map +0 -1
- package/dist/wow/useFetcherSingleQuery.d.ts.map +0 -1
- package/dist/wow/useQuery.d.ts.map +0 -1
- package/dist/wow/useQueryState.d.ts.map +0 -1
- /package/dist/core/{useDebouncedCallback.d.ts → debounced/useDebouncedCallback.d.ts} +0 -0
package/README.zh-CN.md
CHANGED
|
@@ -27,37 +27,47 @@
|
|
|
27
27
|
- [快速开始](#快速开始)
|
|
28
28
|
- [使用方法](#使用方法)
|
|
29
29
|
- [核心 Hooks](#核心-hooks)
|
|
30
|
-
- [useFetcher](#usefetcher-hook)
|
|
31
30
|
- [useExecutePromise](#useexecutepromise)
|
|
32
31
|
- [usePromiseState](#usepromisestate)
|
|
33
|
-
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
39
|
-
|
|
40
|
-
- [
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
- [useRequestId](#userequestid-hook)
|
|
33
|
+
- [useLatest](#uselatest-hook)
|
|
34
|
+
- [useRefs](#userefs-hook)
|
|
35
|
+
- [useQuery](#usequery-hook)
|
|
36
|
+
- [useQueryState](#usequerystate-hook)
|
|
37
|
+
- [useMounted](#usemounted-hook)
|
|
38
|
+
- [useForceUpdate](#useforceupdate-hook)
|
|
39
|
+
- [防抖 Hooks](#防抖-hooks)
|
|
40
|
+
- [useDebouncedCallback](#usedebouncedcallback)
|
|
41
|
+
- [useDebouncedExecutePromise](#usedebouncedexecutepromise)
|
|
42
|
+
- [useDebouncedQuery](#usedebouncedquery)
|
|
43
|
+
- [Fetcher Hooks](#fetcher-hooks)
|
|
44
|
+
- [useFetcher](#usefetcher-hook)
|
|
45
|
+
- [useFetcherQuery](#usefetcherquery-hook)
|
|
46
|
+
- [防抖 Fetcher Hooks](#防抖-fetcher-hooks)
|
|
47
|
+
- [useDebouncedFetcher](#usedebouncedfetcher)
|
|
48
|
+
- [useDebouncedFetcherQuery](#usedebouncedfetcherquery)
|
|
43
49
|
- [存储 Hooks](#存储-hooks)
|
|
44
|
-
- [useKeyStorage](#usekeystorage)
|
|
50
|
+
- [useKeyStorage](#usekeystorage-hook)
|
|
45
51
|
- [useImmerKeyStorage](#useimmerkeystorage-hook)
|
|
46
52
|
- [事件 Hooks](#事件-hooks)
|
|
47
53
|
- [useEventSubscription](#useeventsubscription-hook)
|
|
54
|
+
- [CoSec 安全 Hooks](#cosec-安全-hooks)
|
|
55
|
+
- [useSecurity](#usesecurity-hook)
|
|
56
|
+
- [SecurityProvider](#securityprovider)
|
|
57
|
+
- [useSecurityContext](#usesecuritycontext-hook)
|
|
58
|
+
- [RouteGuard](#routeguard)
|
|
48
59
|
- [Wow 查询 Hooks](#wow-查询-hooks)
|
|
49
60
|
- [基础查询 Hooks](#基础查询-hooks)
|
|
50
61
|
- [useListQuery](#uselistquery-hook)
|
|
51
62
|
- [usePagedQuery](#usepagedquery-hook)
|
|
52
63
|
- [useSingleQuery](#usesinglequery-hook)
|
|
53
64
|
- [useCountQuery](#usecountquery-hook)
|
|
54
|
-
|
|
65
|
+
- [useListStreamQuery](#useliststreamquery-hook)
|
|
66
|
+
- [Fetcher 查询 Hooks](#fetcher-查询-hooks)
|
|
55
67
|
- [useFetcherListQuery](#usefetcherlistquery-hook)
|
|
56
68
|
- [useFetcherPagedQuery](#usefetcherpagedquery-hook)
|
|
57
69
|
- [useFetcherSingleQuery](#usefetchersinglequery-hook)
|
|
58
70
|
- [useFetcherCountQuery](#usefetchercountquery-hook)
|
|
59
|
-
- [流查询 Hooks](#流查询-hooks)
|
|
60
|
-
- [useListStreamQuery](#useliststreamquery-hook)
|
|
61
71
|
- [useFetcherListStreamQuery](#usefetcherliststreamquery-hook)
|
|
62
72
|
- [最佳实践](#最佳实践)
|
|
63
73
|
- [API 参考](#api-参考)
|
|
@@ -101,286 +111,1241 @@ function App() {
|
|
|
101
111
|
|
|
102
112
|
### 核心 Hooks
|
|
103
113
|
|
|
104
|
-
####
|
|
114
|
+
#### useExecutePromise
|
|
105
115
|
|
|
106
|
-
`
|
|
116
|
+
`useExecutePromise` hook 用于管理异步操作,具有自动状态处理、竞态条件保护和 Promise 状态选项。它包含用于取消操作的自动 AbortController 支持。
|
|
107
117
|
|
|
108
118
|
```typescript jsx
|
|
109
|
-
import {
|
|
119
|
+
import { useExecutePromise } from '@ahoo-wang/fetcher-react';
|
|
110
120
|
|
|
111
121
|
const MyComponent = () => {
|
|
112
|
-
const { loading,
|
|
122
|
+
const { loading, result, error, execute, reset, abort } = useExecutePromise<string>({
|
|
123
|
+
onAbort: () => {
|
|
124
|
+
console.log('操作已被中止');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const fetchData = async () => {
|
|
129
|
+
const response = await fetch('/api/data');
|
|
130
|
+
return response.text();
|
|
131
|
+
};
|
|
113
132
|
|
|
114
133
|
const handleFetch = () => {
|
|
115
|
-
execute(
|
|
116
|
-
};
|
|
117
|
-
```
|
|
134
|
+
execute(fetchData); // 使用 Promise 供应商
|
|
135
|
+
};
|
|
118
136
|
|
|
119
|
-
|
|
137
|
+
const handleDirectPromise = () => {
|
|
138
|
+
const promise = fetch('/api/data').then(res => res.text());
|
|
139
|
+
execute(promise); // 使用直接 Promise
|
|
140
|
+
};
|
|
120
141
|
|
|
121
|
-
|
|
142
|
+
const handleAbort = () => {
|
|
143
|
+
abort(); // 手动中止当前操作
|
|
144
|
+
};
|
|
122
145
|
|
|
123
|
-
|
|
146
|
+
if (loading) return <div>加载中...</div>;
|
|
147
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
148
|
+
return (
|
|
149
|
+
<div>
|
|
150
|
+
<button onClick={handleFetch}>使用供应商获取</button>
|
|
151
|
+
<button onClick={handleDirectPromise}>使用 Promise 获取</button>
|
|
152
|
+
<button onClick={handleAbort} disabled={!loading}>中止</button>
|
|
153
|
+
<button onClick={reset}>重置</button>
|
|
154
|
+
{result && <p>{result}</p>}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
```
|
|
124
159
|
|
|
125
|
-
|
|
126
|
-
- **深度对象支持**: 轻松处理嵌套对象和数组
|
|
127
|
-
- **类型安全**: 完整的 TypeScript 支持和编译时错误检查
|
|
128
|
-
- **性能优化**: 利用 Immer 的结构共享和最小化重渲染
|
|
129
|
-
- **自动同步**: 变更自动持久化到存储并跨组件同步
|
|
160
|
+
##### 中止控制器支持
|
|
130
161
|
|
|
131
|
-
|
|
162
|
+
该 hook 会为每个操作自动创建一个 AbortController,并提供管理取消的方法:
|
|
132
163
|
|
|
133
|
-
|
|
164
|
+
- **自动清理**: 组件卸载时操作会自动中止
|
|
165
|
+
- **手动中止**: 使用 `abort()` 方法中止正在进行的操作
|
|
166
|
+
- **onAbort 回调**: 配置在操作中止时触发的回调(手动或自动)
|
|
167
|
+
- **AbortController 访问**: AbortController 会传递给 Promise 供应商以进行高级取消处理
|
|
134
168
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- 处理深度嵌套的数据结构
|
|
169
|
+
#### usePromiseState
|
|
170
|
+
|
|
171
|
+
`usePromiseState` hook 提供 Promise 操作的状态管理,无执行逻辑。支持静态选项和动态选项供应商。
|
|
139
172
|
|
|
140
173
|
```typescript jsx
|
|
141
|
-
import {
|
|
142
|
-
import { useImmerKeyStorage } from '@ahoo-wang/fetcher-react';
|
|
174
|
+
import { usePromiseState, PromiseStatus } from '@ahoo-wang/fetcher-react';
|
|
143
175
|
|
|
144
176
|
const MyComponent = () => {
|
|
145
|
-
const
|
|
146
|
-
theme: string;
|
|
147
|
-
volume: number;
|
|
148
|
-
notifications: boolean;
|
|
149
|
-
shortcuts: { [key: string]: string };
|
|
150
|
-
}>({
|
|
151
|
-
key: 'user-prefs'
|
|
152
|
-
});
|
|
177
|
+
const { status, loading, result, error, setSuccess, setError, setIdle } = usePromiseState<string>();
|
|
153
178
|
|
|
154
|
-
|
|
155
|
-
const
|
|
179
|
+
const handleSuccess = () => setSuccess('数据已加载');
|
|
180
|
+
const handleError = () => setError(new Error('加载失败'));
|
|
156
181
|
|
|
157
182
|
return (
|
|
158
183
|
<div>
|
|
159
|
-
<
|
|
160
|
-
<button onClick={
|
|
161
|
-
|
|
162
|
-
</
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
</
|
|
166
|
-
<button onClick={clearPrefs}>
|
|
167
|
-
清除偏好设置
|
|
168
|
-
</button>
|
|
184
|
+
<button onClick={handleSuccess}>设置成功</button>
|
|
185
|
+
<button onClick={handleError}>设置错误</button>
|
|
186
|
+
<button onClick={setIdle}>重置</button>
|
|
187
|
+
<p>状态: {status}</p>
|
|
188
|
+
{loading && <p>加载中...</p>}
|
|
189
|
+
{result && <p>结果: {result}</p>}
|
|
190
|
+
{error && <p>错误: {error.message}</p>}
|
|
169
191
|
</div>
|
|
170
192
|
);
|
|
171
193
|
};
|
|
172
194
|
```
|
|
173
195
|
|
|
174
|
-
|
|
196
|
+
##### 使用选项供应商的 usePromiseState
|
|
175
197
|
|
|
176
198
|
```typescript jsx
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
199
|
+
import { usePromiseState, PromiseStatus } from '@ahoo-wang/fetcher-react';
|
|
200
|
+
|
|
201
|
+
const MyComponent = () => {
|
|
202
|
+
// 使用选项供应商进行动态配置
|
|
203
|
+
const optionsSupplier = () => ({
|
|
204
|
+
initialStatus: PromiseStatus.IDLE,
|
|
205
|
+
onSuccess: async (result: string) => {
|
|
206
|
+
await saveToAnalytics(result);
|
|
207
|
+
console.log('成功:', result);
|
|
208
|
+
},
|
|
209
|
+
onError: async (error) => {
|
|
210
|
+
await logErrorToServer(error);
|
|
211
|
+
console.error('错误:', error);
|
|
212
|
+
},
|
|
180
213
|
});
|
|
181
214
|
|
|
182
|
-
|
|
183
|
-
const [settings, updateSettings, resetSettings] = useImmerKeyStorage(
|
|
184
|
-
settingsStorage,
|
|
185
|
-
{ volume: 50, muted: false }
|
|
186
|
-
);
|
|
215
|
+
const { setSuccess, setError } = usePromiseState<string>(optionsSupplier);
|
|
187
216
|
|
|
188
217
|
return (
|
|
189
218
|
<div>
|
|
190
|
-
<
|
|
191
|
-
<button onClick={() =>
|
|
192
|
-
draft.volume = Math.min(100, draft.volume + 10);
|
|
193
|
-
draft.muted = false;
|
|
194
|
-
})}>
|
|
195
|
-
增加音量
|
|
196
|
-
</button>
|
|
197
|
-
<button onClick={() => updateSettings(draft => { draft.muted = !draft.muted; })}>
|
|
198
|
-
切换静音
|
|
199
|
-
</button>
|
|
200
|
-
<button onClick={resetSettings}>
|
|
201
|
-
重置为默认值
|
|
202
|
-
</button>
|
|
219
|
+
<button onClick={() => setSuccess('动态成功!')}>设置成功</button>
|
|
220
|
+
<button onClick={() => setError(new Error('动态错误!'))}>设置错误</button>
|
|
203
221
|
</div>
|
|
204
222
|
);
|
|
205
223
|
};
|
|
206
224
|
```
|
|
207
225
|
|
|
208
|
-
####
|
|
226
|
+
#### useRequestId
|
|
209
227
|
|
|
210
|
-
|
|
228
|
+
`useRequestId` hook 提供请求 ID 管理,用于防止异步操作中的竞态条件。
|
|
211
229
|
|
|
212
230
|
```typescript jsx
|
|
213
|
-
|
|
214
|
-
updatePrefs(draft => {
|
|
215
|
-
draft.theme = 'dark';
|
|
216
|
-
draft.notifications = true;
|
|
217
|
-
draft.volume = 75;
|
|
218
|
-
});
|
|
219
|
-
};
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
##### 数组操作
|
|
231
|
+
import { useRequestId } from '@ahoo-wang/fetcher-react';
|
|
223
232
|
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
todos: Array<{ id: number; text: string; done: boolean }>;
|
|
227
|
-
}>({
|
|
228
|
-
key: 'todos',
|
|
229
|
-
});
|
|
233
|
+
const MyComponent = () => {
|
|
234
|
+
const { generate, isLatest, invalidate } = useRequestId();
|
|
230
235
|
|
|
231
|
-
const
|
|
236
|
+
const handleFetch = async () => {
|
|
237
|
+
const requestId = generate();
|
|
232
238
|
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
updateState(draft => {
|
|
236
|
-
draft.todos.push({
|
|
237
|
-
id: Date.now(),
|
|
238
|
-
text,
|
|
239
|
-
done: false,
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
};
|
|
239
|
+
try {
|
|
240
|
+
const result = await fetchData();
|
|
243
241
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
242
|
+
if (isLatest(requestId)) {
|
|
243
|
+
setData(result);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (isLatest(requestId)) {
|
|
247
|
+
setError(error);
|
|
248
|
+
}
|
|
250
249
|
}
|
|
251
|
-
}
|
|
252
|
-
};
|
|
250
|
+
};
|
|
253
251
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
return (
|
|
253
|
+
<div>
|
|
254
|
+
<button onClick={handleFetch}>获取数据</button>
|
|
255
|
+
<button onClick={invalidate}>取消进行中</button>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
259
258
|
};
|
|
260
259
|
```
|
|
261
260
|
|
|
262
|
-
|
|
261
|
+
#### useLatest
|
|
262
|
+
|
|
263
|
+
`useLatest` hook 返回包含最新值的 ref 对象,用于在异步回调中访问当前值。
|
|
263
264
|
|
|
264
265
|
```typescript jsx
|
|
265
|
-
|
|
266
|
-
ui: { theme: string; language: string };
|
|
267
|
-
features: { [key: string]: boolean };
|
|
268
|
-
}>({
|
|
269
|
-
key: 'app-config',
|
|
270
|
-
});
|
|
266
|
+
import { useLatest } from '@ahoo-wang/fetcher-react';
|
|
271
267
|
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
268
|
+
const MyComponent = () => {
|
|
269
|
+
const [count, setCount] = useState(0);
|
|
270
|
+
const latestCount = useLatest(count);
|
|
276
271
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
});
|
|
282
|
-
};
|
|
272
|
+
const handleAsync = async () => {
|
|
273
|
+
await someAsyncOperation();
|
|
274
|
+
console.log('最新计数:', latestCount.current); // 始终是最新的
|
|
275
|
+
};
|
|
283
276
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
277
|
+
return (
|
|
278
|
+
<div>
|
|
279
|
+
<p>计数: {count}</p>
|
|
280
|
+
<button onClick={() => setCount(c => c + 1)}>增加</button>
|
|
281
|
+
<button onClick={handleAsync}>异步日志</button>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
288
284
|
};
|
|
289
285
|
```
|
|
290
286
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
```typescript jsx
|
|
294
|
-
const updateVolume = (newVolume: number) => {
|
|
295
|
-
updateSettings(draft => {
|
|
296
|
-
if (newVolume >= 0 && newVolume <= 100) {
|
|
297
|
-
draft.volume = newVolume;
|
|
298
|
-
draft.muted = false; // 音量改变时取消静音
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
};
|
|
302
|
-
```
|
|
287
|
+
#### useRefs
|
|
303
288
|
|
|
304
|
-
|
|
289
|
+
`useRefs` hook 提供 Map-like 接口用于动态管理多个 React refs。它允许通过键注册、检索和管理 refs,并在组件卸载时自动清理。
|
|
305
290
|
|
|
306
291
|
```typescript jsx
|
|
307
|
-
|
|
308
|
-
const resetToFactorySettings = () => {
|
|
309
|
-
updateSettings(() => ({ volume: 50, muted: false }));
|
|
310
|
-
};
|
|
292
|
+
import { useRefs } from '@ahoo-wang/fetcher-react';
|
|
311
293
|
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
updateSettings(draft => ({ ...draft, volume: 100, muted: false }));
|
|
315
|
-
};
|
|
316
|
-
```
|
|
294
|
+
const MyComponent = () => {
|
|
295
|
+
const refs = useRefs<HTMLDivElement>();
|
|
317
296
|
|
|
318
|
-
|
|
297
|
+
const handleFocus = (key: string) => {
|
|
298
|
+
const element = refs.get(key);
|
|
299
|
+
element?.focus();
|
|
300
|
+
};
|
|
319
301
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
302
|
+
return (
|
|
303
|
+
<div>
|
|
304
|
+
<div ref={refs.register('first')} tabIndex={0}>第一个元素</div>
|
|
305
|
+
<div ref={refs.register('second')} tabIndex={0}>第二个元素</div>
|
|
306
|
+
<button onClick={() => handleFocus('first')}>聚焦第一个</button>
|
|
307
|
+
<button onClick={() => handleFocus('second')}>聚焦第二个</button>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
328
310
|
};
|
|
329
311
|
```
|
|
330
312
|
|
|
331
|
-
|
|
313
|
+
主要特性:
|
|
332
314
|
|
|
333
|
-
|
|
315
|
+
- **动态注册**: 使用字符串、数字或符号键注册 refs
|
|
316
|
+
- **Map-like API**: 完整的 Map 接口,包括 get、set、has、delete 等
|
|
317
|
+
- **自动清理**: 组件卸载时清除 refs
|
|
318
|
+
- **类型安全**: 完整的 TypeScript 支持 ref 类型
|
|
334
319
|
|
|
335
|
-
|
|
336
|
-
- 利用 Immer 的 draft 变更编写可读代码
|
|
337
|
-
- 在单个更新调用中组合多个相关变更
|
|
338
|
-
- 对保证非空状态使用默认值
|
|
339
|
-
- 在更新函数中适当处理错误
|
|
320
|
+
#### useQuery
|
|
340
321
|
|
|
341
|
-
|
|
322
|
+
`useQuery` hook 提供完整的查询基础异步操作管理解决方案,具有自动状态管理和执行控制。
|
|
342
323
|
|
|
343
|
-
|
|
344
|
-
-
|
|
345
|
-
- 不要依赖对象比较的引用相等性
|
|
346
|
-
- 不要用于简单的原始值更新(应使用 `useKeyStorage`)
|
|
324
|
+
```typescript jsx
|
|
325
|
+
import { useQuery } from '@ahoo-wang/fetcher-react';
|
|
347
326
|
|
|
348
|
-
|
|
327
|
+
interface UserQuery {
|
|
328
|
+
id: string;
|
|
329
|
+
}
|
|
349
330
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
331
|
+
interface User {
|
|
332
|
+
id: string;
|
|
333
|
+
name: string;
|
|
334
|
+
}
|
|
354
335
|
|
|
355
|
-
|
|
336
|
+
function UserComponent() {
|
|
337
|
+
const { loading, result, error, execute, setQuery } = useQuery<UserQuery, User>({
|
|
338
|
+
initialQuery: { id: '1' },
|
|
339
|
+
execute: async (query) => {
|
|
340
|
+
const response = await fetch(`/api/users/${query.id}`);
|
|
341
|
+
return response.json();
|
|
342
|
+
},
|
|
343
|
+
autoExecute: true,
|
|
344
|
+
});
|
|
356
345
|
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
theme: 'light' | 'dark' | 'auto';
|
|
361
|
-
volume: number; // 0-100
|
|
362
|
-
notifications: boolean;
|
|
363
|
-
shortcuts: Record<string, string>;
|
|
364
|
-
};
|
|
346
|
+
const handleUserChange = (userId: string) => {
|
|
347
|
+
setQuery({ id: userId }); // 如果 autoExecute 为 true 会自动执行
|
|
348
|
+
};
|
|
365
349
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
350
|
+
if (loading) return <div>加载中...</div>;
|
|
351
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
352
|
+
return (
|
|
353
|
+
<div>
|
|
354
|
+
<button onClick={() => handleUserChange('2')}>加载用户 2</button>
|
|
355
|
+
{result && <p>用户: {result.name}</p>}
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
369
360
|
|
|
370
|
-
|
|
371
|
-
|
|
361
|
+
#### useQueryState
|
|
362
|
+
|
|
363
|
+
`useQueryState` hook 提供查询参数的状态管理,具有自动执行功能。
|
|
364
|
+
|
|
365
|
+
```typescript jsx
|
|
366
|
+
import { useQueryState } from '@ahoo-wang/fetcher-react';
|
|
367
|
+
|
|
368
|
+
interface UserQuery {
|
|
369
|
+
id: string;
|
|
370
|
+
name?: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function UserComponent() {
|
|
374
|
+
const executeQuery = async (query: UserQuery) => {
|
|
375
|
+
// 执行查询逻辑
|
|
376
|
+
console.log('执行查询:', query);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const { getQuery, setQuery } = useQueryState<UserQuery>({
|
|
380
|
+
initialQuery: { id: '1' },
|
|
381
|
+
autoExecute: true,
|
|
382
|
+
execute: executeQuery,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const handleQueryChange = (newQuery: UserQuery) => {
|
|
386
|
+
setQuery(newQuery); // 如果 autoExecute 为 true 会自动执行
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const currentQuery = getQuery(); // 获取当前查询参数
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div>
|
|
393
|
+
<button onClick={() => handleQueryChange({ id: '2', name: 'John' })}>
|
|
394
|
+
更新查询
|
|
395
|
+
</button>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### useMounted
|
|
402
|
+
|
|
403
|
+
`useMounted` hook 提供检查组件是否仍挂载的方法,用于避免在卸载组件上更新状态。
|
|
404
|
+
|
|
405
|
+
```typescript jsx
|
|
406
|
+
import { useMounted } from '@ahoo-wang/fetcher-react';
|
|
407
|
+
|
|
408
|
+
const MyComponent = () => {
|
|
409
|
+
const isMounted = useMounted();
|
|
410
|
+
|
|
411
|
+
const handleAsyncOperation = async () => {
|
|
412
|
+
const result = await someAsyncOperation();
|
|
413
|
+
|
|
414
|
+
// 检查组件是否仍挂载后再更新状态
|
|
415
|
+
if (isMounted()) {
|
|
416
|
+
setData(result);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<div>
|
|
422
|
+
<button onClick={handleAsyncOperation}>执行异步操作</button>
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### useForceUpdate
|
|
429
|
+
|
|
430
|
+
`useForceUpdate` hook 提供强制组件重新渲染的方法,用于在需要基于外部变化触发渲染时使用。
|
|
431
|
+
|
|
432
|
+
```typescript jsx
|
|
433
|
+
import { useForceUpdate } from '@ahoo-wang/fetcher-react';
|
|
434
|
+
|
|
435
|
+
const MyComponent = () => {
|
|
436
|
+
const forceUpdate = useForceUpdate();
|
|
437
|
+
|
|
438
|
+
const handleExternalChange = () => {
|
|
439
|
+
// 执行不会触发重新渲染的外部操作
|
|
440
|
+
updateExternalState();
|
|
441
|
+
|
|
442
|
+
// 强制组件重新渲染以反映变化
|
|
443
|
+
forceUpdate();
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<div>
|
|
448
|
+
<button onClick={handleExternalChange}>强制更新</button>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
};
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### 防抖 Hooks
|
|
455
|
+
|
|
456
|
+
🚀 **React 应用的高级防抖** - 强大的 hooks,将防抖与异步操作结合,为 API 调用、用户交互和 Promise 执行提供无缝的速率限制。
|
|
457
|
+
|
|
458
|
+
#### useDebouncedCallback
|
|
459
|
+
|
|
460
|
+
React hook,为任何回调函数提供防抖版本,支持前缘/后缘执行选项。
|
|
461
|
+
|
|
462
|
+
```typescript jsx
|
|
463
|
+
import { useDebouncedCallback } from '@ahoo-wang/fetcher-react';
|
|
464
|
+
|
|
465
|
+
const SearchComponent = () => {
|
|
466
|
+
const { run: debouncedSearch, cancel, isPending } = useDebouncedCallback(
|
|
467
|
+
async (query: string) => {
|
|
468
|
+
const response = await fetch(`/api/search?q=${query}`);
|
|
469
|
+
const results = await response.json();
|
|
470
|
+
console.log('搜索结果:', results);
|
|
471
|
+
},
|
|
472
|
+
{ delay: 300 }
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const handleSearch = (query: string) => {
|
|
476
|
+
if (query.trim()) {
|
|
477
|
+
debouncedSearch(query);
|
|
478
|
+
} else {
|
|
479
|
+
cancel(); // 取消任何待处理的搜索
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<div>
|
|
485
|
+
<input
|
|
486
|
+
type="text"
|
|
487
|
+
placeholder="搜索..."
|
|
488
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
489
|
+
/>
|
|
490
|
+
{isPending() && <div>搜索中...</div>}
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**配置选项:**
|
|
497
|
+
|
|
498
|
+
- `delay`: 执行前的延迟毫秒数(必需,正数)
|
|
499
|
+
- `leading`: 第一次调用时立即执行(默认:false)
|
|
500
|
+
- `trailing`: 最后一次调用后延迟执行(默认:true)
|
|
501
|
+
|
|
502
|
+
#### useDebouncedExecutePromise
|
|
503
|
+
|
|
504
|
+
将 Promise 执行与防抖功能结合,适用于 API 调用和异步操作。
|
|
505
|
+
|
|
506
|
+
```typescript jsx
|
|
507
|
+
import { useDebouncedExecutePromise } from '@ahoo-wang/fetcher-react';
|
|
508
|
+
|
|
509
|
+
const DataFetcher = () => {
|
|
510
|
+
const { loading, result, error, run } = useDebouncedExecutePromise({
|
|
511
|
+
debounce: { delay: 300 },
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const handleLoadUser = (userId: string) => {
|
|
515
|
+
run(async () => {
|
|
516
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
517
|
+
return response.json();
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
return (
|
|
522
|
+
<div>
|
|
523
|
+
<button onClick={() => handleLoadUser('user123')}>
|
|
524
|
+
加载用户
|
|
525
|
+
</button>
|
|
526
|
+
{loading && <div>加载中...</div>}
|
|
527
|
+
{error && <div>错误: {error.message}</div>}
|
|
528
|
+
{result && <div>用户: {result.name}</div>}
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
};
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### useDebouncedQuery
|
|
535
|
+
|
|
536
|
+
将通用查询执行与防抖结合,适用于自定义查询操作,您希望根据查询参数防抖执行。
|
|
537
|
+
|
|
538
|
+
```typescript jsx
|
|
539
|
+
import { useDebouncedQuery } from '@ahoo-wang/fetcher-react';
|
|
540
|
+
|
|
541
|
+
interface SearchQuery {
|
|
542
|
+
keyword: string;
|
|
543
|
+
limit: number;
|
|
544
|
+
filters?: { category?: string };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
interface SearchResult {
|
|
548
|
+
items: Array<{ id: string; title: string }>;
|
|
549
|
+
total: number;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const SearchComponent = () => {
|
|
553
|
+
const {
|
|
554
|
+
loading,
|
|
555
|
+
result,
|
|
556
|
+
error,
|
|
557
|
+
run,
|
|
558
|
+
cancel,
|
|
559
|
+
isPending,
|
|
560
|
+
setQuery,
|
|
561
|
+
getQuery,
|
|
562
|
+
} = useDebouncedQuery<SearchQuery, SearchResult>({
|
|
563
|
+
initialQuery: { keyword: '', limit: 10 },
|
|
564
|
+
execute: async (query) => {
|
|
565
|
+
const response = await fetch('/api/search', {
|
|
566
|
+
method: 'POST',
|
|
567
|
+
body: JSON.stringify(query),
|
|
568
|
+
headers: { 'Content-Type': 'application/json' },
|
|
569
|
+
});
|
|
570
|
+
return response.json();
|
|
571
|
+
},
|
|
572
|
+
debounce: { delay: 300 }, // 防抖 300ms
|
|
573
|
+
autoExecute: false, // 挂载时不执行
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const handleSearch = (keyword: string) => {
|
|
577
|
+
setQuery({ keyword, limit: 10 }); // 如果 autoExecute 为 true,这将触发防抖执行
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const handleManualSearch = () => {
|
|
581
|
+
run(); // 使用当前查询手动防抖执行
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const handleCancel = () => {
|
|
585
|
+
cancel(); // 取消任何待处理的防抖执行
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
if (loading) return <div>搜索中...</div>;
|
|
589
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<div>
|
|
593
|
+
<input
|
|
594
|
+
type="text"
|
|
595
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
596
|
+
placeholder="搜索..."
|
|
597
|
+
/>
|
|
598
|
+
<button onClick={handleManualSearch} disabled={isPending()}>
|
|
599
|
+
{isPending() ? '搜索中...' : '搜索'}
|
|
600
|
+
</button>
|
|
601
|
+
<button onClick={handleCancel}>取消</button>
|
|
602
|
+
{result && (
|
|
603
|
+
<div>
|
|
604
|
+
找到 {result.total} 项:
|
|
605
|
+
{result.items.map(item => (
|
|
606
|
+
<div key={item.id}>{item.title}</div>
|
|
607
|
+
))}
|
|
608
|
+
</div>
|
|
609
|
+
)}
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
};
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
**主要特性:**
|
|
616
|
+
|
|
617
|
+
- **查询状态管理**: 使用 `setQuery` 和 `getQuery` 自动查询参数处理
|
|
618
|
+
- **防抖执行**: 在快速查询更改期间防止过多操作
|
|
619
|
+
- **自动执行**: 可选的在查询参数更改时自动执行
|
|
620
|
+
- **手动控制**: `run()` 用于手动执行,`cancel()` 用于取消
|
|
621
|
+
- **待处理状态**: `isPending()` 检查防抖调用是否排队
|
|
622
|
+
- **自定义执行**: 灵活的 execute 函数用于任何查询操作
|
|
623
|
+
|
|
624
|
+
### Fetcher Hooks
|
|
625
|
+
|
|
626
|
+
#### useFetcher
|
|
627
|
+
|
|
628
|
+
`useFetcher` hook 提供完整的数据获取功能,具有自动状态管理、竞态条件保护和灵活的配置选项。它包含从 `useExecutePromise` 继承的内置 AbortController 支持。
|
|
629
|
+
|
|
630
|
+
```typescript jsx
|
|
631
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
632
|
+
|
|
633
|
+
const MyComponent = () => {
|
|
634
|
+
const { loading, error, result, execute, abort } = useFetcher<string>({
|
|
635
|
+
onAbort: () => {
|
|
636
|
+
console.log('获取操作已被中止');
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const handleFetch = () => {
|
|
641
|
+
execute({ url: '/api/users', method: 'GET' });
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const handleAbort = () => {
|
|
645
|
+
abort(); // 取消当前获取操作
|
|
646
|
+
};
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
#### 自动执行示例
|
|
650
|
+
|
|
651
|
+
```typescript jsx
|
|
652
|
+
import { useListQuery } from '@ahoo-wang/fetcher-react';
|
|
653
|
+
|
|
654
|
+
const MyComponent = () => {
|
|
655
|
+
const { result, loading, error, execute, setCondition } = useListQuery({
|
|
656
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
|
|
657
|
+
list: async (listQuery) => fetchListData(listQuery),
|
|
658
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// 查询将在组件挂载时自动执行
|
|
662
|
+
// 您仍然可以使用 execute() 手动触发或更新条件
|
|
663
|
+
|
|
664
|
+
if (loading) return <div>加载中...</div>;
|
|
665
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
666
|
+
|
|
667
|
+
return (
|
|
668
|
+
<div>
|
|
669
|
+
<ul>
|
|
670
|
+
{result?.map((item, index) => (
|
|
671
|
+
<li key={index}>{item.name}</li>
|
|
672
|
+
))}
|
|
673
|
+
</ul>
|
|
674
|
+
</div>
|
|
675
|
+
);
|
|
676
|
+
};
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
#### useFetcherQuery
|
|
680
|
+
|
|
681
|
+
`useFetcherQuery` hook 提供构建与 Fetcher 库集成的专用查询 hooks 的基础。
|
|
682
|
+
|
|
683
|
+
```typescript jsx
|
|
684
|
+
import { useFetcherQuery } from '@ahoo-wang/fetcher-react';
|
|
685
|
+
|
|
686
|
+
const MyComponent = () => {
|
|
687
|
+
const { data, loading, error, execute } = useFetcherQuery({
|
|
688
|
+
url: '/api/data',
|
|
689
|
+
initialQuery: { /* 查询参数 */ },
|
|
690
|
+
execute: async (query) => {
|
|
691
|
+
// 自定义执行逻辑
|
|
692
|
+
return fetchData(query);
|
|
693
|
+
},
|
|
694
|
+
autoExecute: true,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
if (loading) return <div>加载中...</div>;
|
|
698
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
699
|
+
|
|
700
|
+
return (
|
|
701
|
+
<div>
|
|
702
|
+
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
703
|
+
</div>
|
|
704
|
+
);
|
|
705
|
+
};
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### 防抖 Fetcher Hooks
|
|
709
|
+
|
|
710
|
+
#### useDebouncedFetcher
|
|
711
|
+
|
|
712
|
+
专门的 hook,将 HTTP 获取与防抖结合,基于核心 fetcher 库构建。
|
|
713
|
+
|
|
714
|
+
```typescript jsx
|
|
715
|
+
import { useDebouncedFetcher } from '@ahoo-wang/fetcher-react';
|
|
716
|
+
|
|
717
|
+
const SearchInput = () => {
|
|
718
|
+
const [query, setQuery] = useState('');
|
|
719
|
+
const { loading, result, error, run } = useDebouncedFetcher({
|
|
720
|
+
debounce: { delay: 300 },
|
|
721
|
+
onSuccess: (data) => {
|
|
722
|
+
setSearchResults(data.results);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const handleChange = (value: string) => {
|
|
727
|
+
setQuery(value);
|
|
728
|
+
if (value.trim()) {
|
|
729
|
+
run({
|
|
730
|
+
url: '/api/search',
|
|
731
|
+
method: 'GET',
|
|
732
|
+
params: { q: value }
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
return (
|
|
738
|
+
<div>
|
|
739
|
+
<input
|
|
740
|
+
value={query}
|
|
741
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
742
|
+
placeholder="搜索..."
|
|
743
|
+
/>
|
|
744
|
+
{loading && <div>搜索中...</div>}
|
|
745
|
+
{error && <div>错误: {error.message}</div>}
|
|
746
|
+
{result && <SearchResults data={result} />}
|
|
747
|
+
</div>
|
|
748
|
+
);
|
|
749
|
+
};
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**防抖策略:**
|
|
753
|
+
|
|
754
|
+
- **前缘**: 第一次调用时立即执行,然后对后续调用进行防抖
|
|
755
|
+
- **后缘**: 最后一次调用后延迟执行(默认行为)
|
|
756
|
+
- **前缘 + 后缘**: 立即执行,如果再次调用则在延迟后再次执行
|
|
757
|
+
|
|
758
|
+
#### useDebouncedFetcherQuery
|
|
759
|
+
|
|
760
|
+
将基于查询的 HTTP 获取与防抖结合,非常适合搜索输入和动态查询场景,您希望根据查询参数防抖 API 调用。
|
|
761
|
+
|
|
762
|
+
```typescript jsx
|
|
763
|
+
import { useDebouncedFetcherQuery } from '@ahoo-wang/fetcher-react';
|
|
764
|
+
|
|
765
|
+
interface SearchQuery {
|
|
766
|
+
keyword: string;
|
|
767
|
+
limit: number;
|
|
768
|
+
filters?: { category?: string };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
interface SearchResult {
|
|
772
|
+
items: Array<{ id: string; title: string }>;
|
|
773
|
+
total: number;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const SearchComponent = () => {
|
|
777
|
+
const {
|
|
778
|
+
loading,
|
|
779
|
+
result,
|
|
780
|
+
error,
|
|
781
|
+
run,
|
|
782
|
+
cancel,
|
|
783
|
+
isPending,
|
|
784
|
+
setQuery,
|
|
785
|
+
getQuery,
|
|
786
|
+
} = useDebouncedFetcherQuery<SearchQuery, SearchResult>({
|
|
787
|
+
url: '/api/search',
|
|
788
|
+
initialQuery: { keyword: '', limit: 10 },
|
|
789
|
+
debounce: { delay: 300 }, // 防抖 300ms
|
|
790
|
+
autoExecute: false, // 挂载时不执行
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const handleSearch = (keyword: string) => {
|
|
794
|
+
setQuery({ keyword, limit: 10 }); // 如果 autoExecute 为 true,这将触发防抖执行
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const handleManualSearch = () => {
|
|
798
|
+
run(); // 使用当前查询手动防抖执行
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const handleCancel = () => {
|
|
802
|
+
cancel(); // 取消任何待处理的防抖执行
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
if (loading) return <div>搜索中...</div>;
|
|
806
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
807
|
+
|
|
808
|
+
return (
|
|
809
|
+
<div>
|
|
810
|
+
<input
|
|
811
|
+
type="text"
|
|
812
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
813
|
+
placeholder="搜索..."
|
|
814
|
+
/>
|
|
815
|
+
<button onClick={handleManualSearch} disabled={isPending()}>
|
|
816
|
+
{isPending() ? '搜索中...' : '搜索'}
|
|
817
|
+
</button>
|
|
818
|
+
<button onClick={handleCancel}>取消</button>
|
|
819
|
+
{result && (
|
|
820
|
+
<div>
|
|
821
|
+
找到 {result.total} 项:
|
|
822
|
+
{result.items.map(item => (
|
|
823
|
+
<div key={item.id}>{item.title}</div>
|
|
824
|
+
))}
|
|
825
|
+
</div>
|
|
826
|
+
)}
|
|
827
|
+
</div>
|
|
828
|
+
);
|
|
829
|
+
};
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**主要特性:**
|
|
833
|
+
|
|
834
|
+
- **查询状态管理**: 使用 `setQuery` 和 `getQuery` 自动查询参数处理
|
|
835
|
+
- **防抖执行**: 在快速用户输入期间防止过多 API 调用
|
|
836
|
+
- **自动执行**: 可选的在查询参数更改时自动执行
|
|
837
|
+
- **手动控制**: `run()` 用于手动执行,`cancel()` 用于取消
|
|
838
|
+
- **待处理状态**: `isPending()` 检查防抖调用是否排队
|
|
839
|
+
|
|
840
|
+
### 存储 Hooks
|
|
841
|
+
|
|
842
|
+
#### useKeyStorage
|
|
843
|
+
|
|
844
|
+
`useKeyStorage` hook 为 KeyStorage 实例提供响应式状态管理。它订阅存储更改并返回当前值以及设置器函数。可选择接受默认值以在存储为空时使用。
|
|
845
|
+
|
|
846
|
+
```typescript jsx
|
|
847
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
848
|
+
import { useKeyStorage } from '@ahoo-wang/fetcher-react';
|
|
849
|
+
|
|
850
|
+
const MyComponent = () => {
|
|
851
|
+
const keyStorage = new KeyStorage<string>({ key: 'my-key' });
|
|
852
|
+
|
|
853
|
+
// 不使用默认值 - 可能为 null
|
|
854
|
+
const [value, setValue] = useKeyStorage(keyStorage);
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<div>
|
|
858
|
+
<p>当前值: {value || '无存储值'}</p>
|
|
859
|
+
<button onClick={() => setValue('new value')}>
|
|
860
|
+
更新值
|
|
861
|
+
</button>
|
|
862
|
+
</div>
|
|
863
|
+
);
|
|
864
|
+
};
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
#### 使用默认值
|
|
868
|
+
|
|
869
|
+
```typescript jsx
|
|
870
|
+
const MyComponent = () => {
|
|
871
|
+
const keyStorage = new KeyStorage<string>({ key: 'theme' });
|
|
872
|
+
|
|
873
|
+
// 使用默认值 - 保证不为 null
|
|
874
|
+
const [theme, setTheme] = useKeyStorage(keyStorage, 'light');
|
|
875
|
+
|
|
876
|
+
return (
|
|
877
|
+
<div className={theme}>
|
|
878
|
+
<p>当前主题: {theme}</p>
|
|
879
|
+
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
|
880
|
+
切换主题
|
|
881
|
+
</button>
|
|
882
|
+
</div>
|
|
883
|
+
);
|
|
884
|
+
};
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
#### 更多示例
|
|
888
|
+
|
|
889
|
+
```typescript jsx
|
|
890
|
+
// 处理不同类型的值
|
|
891
|
+
const numberStorage = new KeyStorage<number>({ key: 'counter' });
|
|
892
|
+
const [count, setCount] = useKeyStorage(numberStorage, 0); // 默认为 0
|
|
893
|
+
|
|
894
|
+
// 处理对象
|
|
895
|
+
interface User {
|
|
896
|
+
id: string;
|
|
897
|
+
name: string;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const userStorage = new KeyStorage<User>({ key: 'current-user' });
|
|
901
|
+
const [user, setUser] = useKeyStorage(userStorage, { id: '', name: '访客' });
|
|
902
|
+
|
|
903
|
+
// 复杂状态管理
|
|
904
|
+
const settingsStorage = new KeyStorage<{ volume: number; muted: boolean }>({
|
|
905
|
+
key: 'audio-settings',
|
|
906
|
+
});
|
|
907
|
+
const [settings, setSettings] = useKeyStorage(settingsStorage, {
|
|
908
|
+
volume: 50,
|
|
909
|
+
muted: false,
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// 更新特定属性
|
|
913
|
+
const updateVolume = (newVolume: number) => {
|
|
914
|
+
setSettings({ ...settings, volume: newVolume });
|
|
915
|
+
};
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
### useImmerKeyStorage
|
|
919
|
+
|
|
920
|
+
🚀 **Immer 驱动的不可变状态管理** - `useImmerKeyStorage` hook 通过集成 Immer 的 `produce` 函数扩展了 `useKeyStorage`,允许开发者以直观的"可变"方式更新存储值,同时在底层保持不可变性。非常适合复杂对象的操作,具有自动存储同步功能。
|
|
921
|
+
|
|
922
|
+
#### 主要优势
|
|
923
|
+
|
|
924
|
+
- **直观的变更语法**: 编写看起来可变的代码,但产生不可变更新
|
|
925
|
+
- **深度对象支持**: 轻松处理嵌套对象和数组
|
|
926
|
+
- **类型安全**: 完整的 TypeScript 支持和编译时错误检查
|
|
927
|
+
- **性能优化**: 利用 Immer 的结构共享和最小化重渲染
|
|
928
|
+
- **自动同步**: 变更自动持久化到存储并跨组件同步
|
|
929
|
+
|
|
930
|
+
#### 使用场景
|
|
931
|
+
|
|
932
|
+
在需要以下情况时选择 `useImmerKeyStorage` 而不是 `useKeyStorage`:
|
|
933
|
+
|
|
934
|
+
- 更新嵌套对象属性
|
|
935
|
+
- 执行复杂的数组操作(push、splice 等)
|
|
936
|
+
- 原子性地进行多个相关变更
|
|
937
|
+
- 处理深度嵌套的数据结构
|
|
938
|
+
|
|
939
|
+
```typescript jsx
|
|
940
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
941
|
+
import { useImmerKeyStorage } from '@ahoo-wang/fetcher-react';
|
|
942
|
+
|
|
943
|
+
const MyComponent = () => {
|
|
944
|
+
const prefsStorage = new KeyStorage<{
|
|
945
|
+
theme: string;
|
|
946
|
+
volume: number;
|
|
947
|
+
notifications: boolean;
|
|
948
|
+
shortcuts: { [key: string]: string };
|
|
949
|
+
}>({
|
|
950
|
+
key: 'user-prefs'
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// 不使用默认值 - 可能为 null
|
|
954
|
+
const [prefs, updatePrefs, clearPrefs] = useImmerKeyStorage(prefsStorage);
|
|
955
|
+
|
|
956
|
+
return (
|
|
957
|
+
<div>
|
|
958
|
+
<p>主题: {prefs?.theme || '默认'}</p>
|
|
959
|
+
<button onClick={() => updatePrefs(draft => { draft.theme = 'dark'; })}>
|
|
960
|
+
切换到深色主题
|
|
961
|
+
</button>
|
|
962
|
+
<button onClick={() => updatePrefs(draft => { draft.volume += 10; })}>
|
|
963
|
+
增加音量
|
|
964
|
+
</button>
|
|
965
|
+
<button onClick={clearPrefs}>
|
|
966
|
+
清除偏好设置
|
|
967
|
+
</button>
|
|
968
|
+
</div>
|
|
969
|
+
);
|
|
970
|
+
};
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
#### 使用默认值
|
|
974
|
+
|
|
975
|
+
```typescript jsx
|
|
976
|
+
const AudioControls = () => {
|
|
977
|
+
const settingsStorage = new KeyStorage<{ volume: number; muted: boolean }>({
|
|
978
|
+
key: 'audio-settings'
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// 使用默认值 - 保证不为 null
|
|
982
|
+
const [settings, updateSettings, resetSettings] = useImmerKeyStorage(
|
|
983
|
+
settingsStorage,
|
|
984
|
+
{ volume: 50, muted: false }
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
return (
|
|
988
|
+
<div>
|
|
989
|
+
<p>音量: {settings.volume}%</p>
|
|
990
|
+
<button onClick={() => updateSettings(draft => {
|
|
991
|
+
draft.volume = Math.min(100, draft.volume + 10);
|
|
992
|
+
draft.muted = false;
|
|
993
|
+
})}>
|
|
994
|
+
增加音量
|
|
995
|
+
</button>
|
|
996
|
+
<button onClick={() => updateSettings(draft => { draft.muted = !draft.muted; })}>
|
|
997
|
+
切换静音
|
|
998
|
+
</button>
|
|
999
|
+
<button onClick={resetSettings}>
|
|
1000
|
+
重置为默认值
|
|
1001
|
+
</button>
|
|
1002
|
+
</div>
|
|
1003
|
+
);
|
|
1004
|
+
};
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
#### 高级用法模式
|
|
1008
|
+
|
|
1009
|
+
##### 批量更新
|
|
1010
|
+
|
|
1011
|
+
```typescript jsx
|
|
1012
|
+
const updateUserProfile = () => {
|
|
1013
|
+
updatePrefs(draft => {
|
|
1014
|
+
draft.theme = 'dark';
|
|
1015
|
+
draft.notifications = true;
|
|
1016
|
+
draft.volume = 75;
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
##### 数组操作
|
|
1022
|
+
|
|
1023
|
+
```typescript jsx
|
|
1024
|
+
const todoStorage = new KeyStorage<{
|
|
1025
|
+
todos: Array<{ id: number; text: string; done: boolean }>;
|
|
1026
|
+
}>({
|
|
1027
|
+
key: 'todos',
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
const [state, updateState] = useImmerKeyStorage(todoStorage, { todos: [] });
|
|
1031
|
+
|
|
1032
|
+
// 添加新待办事项
|
|
1033
|
+
const addTodo = (text: string) => {
|
|
1034
|
+
updateState(draft => {
|
|
1035
|
+
draft.todos.push({
|
|
1036
|
+
id: Date.now(),
|
|
1037
|
+
text,
|
|
1038
|
+
done: false,
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
// 切换待办事项状态
|
|
1044
|
+
const toggleTodo = (id: number) => {
|
|
1045
|
+
updateState(draft => {
|
|
1046
|
+
const todo = draft.todos.find(t => t.id === id);
|
|
1047
|
+
if (todo) {
|
|
1048
|
+
todo.done = !todo.done;
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
// 清除已完成的待办事项
|
|
1054
|
+
const clearCompleted = () => {
|
|
1055
|
+
updateState(draft => {
|
|
1056
|
+
draft.todos = draft.todos.filter(todo => !todo.done);
|
|
1057
|
+
});
|
|
1058
|
+
};
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
##### 嵌套对象更新
|
|
1062
|
+
|
|
1063
|
+
```typescript jsx
|
|
1064
|
+
const configStorage = new KeyStorage<{
|
|
1065
|
+
ui: { theme: string; language: string };
|
|
1066
|
+
features: { [key: string]: boolean };
|
|
1067
|
+
}>({
|
|
1068
|
+
key: 'app-config',
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const [config, updateConfig] = useImmerKeyStorage(configStorage, {
|
|
1072
|
+
ui: { theme: 'light', language: 'zh' },
|
|
1073
|
+
features: {},
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// 更新嵌套属性
|
|
1077
|
+
const updateTheme = (theme: string) => {
|
|
1078
|
+
updateConfig(draft => {
|
|
1079
|
+
draft.ui.theme = theme;
|
|
1080
|
+
});
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const toggleFeature = (feature: string) => {
|
|
1084
|
+
updateConfig(draft => {
|
|
1085
|
+
draft.features[feature] = !draft.features[feature];
|
|
1086
|
+
});
|
|
1087
|
+
};
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
##### 带验证的条件更新
|
|
1091
|
+
|
|
1092
|
+
```typescript jsx
|
|
1093
|
+
const updateVolume = (newVolume: number) => {
|
|
1094
|
+
updateSettings(draft => {
|
|
1095
|
+
if (newVolume >= 0 && newVolume <= 100) {
|
|
1096
|
+
draft.volume = newVolume;
|
|
1097
|
+
draft.muted = false; // 音量改变时取消静音
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
};
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
##### 返回新值
|
|
1104
|
+
|
|
1105
|
+
```typescript jsx
|
|
1106
|
+
// 替换整个状态
|
|
1107
|
+
const resetToFactorySettings = () => {
|
|
1108
|
+
updateSettings(() => ({ volume: 50, muted: false }));
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
// 计算更新
|
|
1112
|
+
const setMaxVolume = () => {
|
|
1113
|
+
updateSettings(draft => ({ ...draft, volume: 100, muted: false }));
|
|
1114
|
+
};
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
##### 错误处理
|
|
1118
|
+
|
|
1119
|
+
```typescript jsx
|
|
1120
|
+
const safeUpdate = (updater: (draft: any) => void) => {
|
|
1121
|
+
try {
|
|
1122
|
+
updatePrefs(updater);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
console.error('更新偏好设置失败:', error);
|
|
1125
|
+
// 适当处理错误
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
#### 最佳实践
|
|
1131
|
+
|
|
1132
|
+
##### ✅ 推荐做法
|
|
1133
|
+
|
|
1134
|
+
- 用于复杂对象更新和数组操作
|
|
1135
|
+
- 利用 Immer 的 draft 变更编写可读代码
|
|
1136
|
+
- 在单个更新调用中组合多个相关变更
|
|
1137
|
+
- 对保证非空状态使用默认值
|
|
1138
|
+
- 在更新函数中适当处理错误
|
|
1139
|
+
|
|
1140
|
+
##### ❌ 避免做法
|
|
1141
|
+
|
|
1142
|
+
- 不要直接用赋值修改 draft 参数(`draft = newValue`)
|
|
1143
|
+
- 不要在更新函数中执行副作用
|
|
1144
|
+
- 不要依赖对象比较的引用相等性
|
|
1145
|
+
- 不要用于简单的原始值更新(应使用 `useKeyStorage`)
|
|
1146
|
+
|
|
1147
|
+
##### 性能提示
|
|
1148
|
+
|
|
1149
|
+
- 将相关更新批量处理以最小化存储操作
|
|
1150
|
+
- 当新状态依赖于之前状态时使用函数式更新
|
|
1151
|
+
- 如果更新函数经常重新创建,考虑使用 `useCallback`
|
|
1152
|
+
- 如果处理非常大的对象,请分析更新性能
|
|
1153
|
+
|
|
1154
|
+
##### TypeScript 集成
|
|
1155
|
+
|
|
1156
|
+
```typescript jsx
|
|
1157
|
+
// 为更好的安全性定义严格类型
|
|
1158
|
+
type UserPreferences = {
|
|
1159
|
+
theme: 'light' | 'dark' | 'auto';
|
|
1160
|
+
volume: number; // 0-100
|
|
1161
|
+
notifications: boolean;
|
|
1162
|
+
shortcuts: Record<string, string>;
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
const prefsStorage = new KeyStorage<UserPreferences>({
|
|
1166
|
+
key: 'user-prefs',
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// TypeScript 将捕获无效更新
|
|
1170
|
+
const [prefs, updatePrefs] = useImmerKeyStorage(prefsStorage);
|
|
372
1171
|
|
|
373
1172
|
// 这将导致 TypeScript 错误:
|
|
374
1173
|
// updatePrefs(draft => { draft.theme = 'invalid'; });
|
|
375
1174
|
```
|
|
376
1175
|
|
|
377
|
-
|
|
1176
|
+
### 事件 Hooks
|
|
1177
|
+
|
|
1178
|
+
#### useEventSubscription
|
|
1179
|
+
|
|
1180
|
+
`useEventSubscription` hook 为类型化事件总线提供了 React 接口。它自动管理订阅生命周期,同时提供手动控制功能以增加灵活性。
|
|
1181
|
+
|
|
1182
|
+
```typescript jsx
|
|
1183
|
+
import { useEventSubscription } from '@ahoo-wang/fetcher-react';
|
|
1184
|
+
import { eventBus } from './eventBus';
|
|
1185
|
+
|
|
1186
|
+
function MyComponent() {
|
|
1187
|
+
const { subscribe, unsubscribe } = useEventSubscription({
|
|
1188
|
+
bus: eventBus,
|
|
1189
|
+
handler: {
|
|
1190
|
+
name: 'myEvent',
|
|
1191
|
+
handle: (event) => {
|
|
1192
|
+
console.log('收到事件:', event);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// hook 在组件挂载时自动订阅,在卸载时自动取消订阅
|
|
1198
|
+
// 如需要,您也可以手动控制订阅
|
|
1199
|
+
const handleToggleSubscription = () => {
|
|
1200
|
+
if (someCondition) {
|
|
1201
|
+
subscribe();
|
|
1202
|
+
} else {
|
|
1203
|
+
unsubscribe();
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
return <div>我的组件</div>;
|
|
1208
|
+
}
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
关键特性:
|
|
1212
|
+
|
|
1213
|
+
- **自动生命周期管理**: 在组件挂载时自动订阅,在卸载时自动取消订阅
|
|
1214
|
+
- **手动控制**: 提供 `subscribe` 和 `unsubscribe` 函数以进行额外控制
|
|
1215
|
+
- **类型安全**: 完全支持 TypeScript,具有泛型事件类型
|
|
1216
|
+
- **错误处理**: 对失败的订阅尝试记录警告
|
|
1217
|
+
- **事件总线集成**: 与 `@ahoo-wang/fetcher-eventbus` TypedEventBus 实例无缝配合
|
|
1218
|
+
|
|
1219
|
+
### CoSec 安全 Hooks
|
|
1220
|
+
|
|
1221
|
+
🛡️ **企业安全集成** - 强大的 React hooks,用于使用 CoSec 令牌管理认证状态,提供与企业安全系统的无缝集成和自动令牌生命周期管理。
|
|
1222
|
+
|
|
1223
|
+
#### useSecurity
|
|
1224
|
+
|
|
1225
|
+
`useSecurity` hook 使用 CoSec 令牌提供对认证状态和操作的响应式访问。它与 TokenStorage 集成以持久化令牌,并在令牌更改时响应式更新状态。
|
|
1226
|
+
|
|
1227
|
+
```typescript jsx
|
|
1228
|
+
import { useSecurity } from '@ahoo-wang/fetcher-react';
|
|
1229
|
+
import { tokenStorage } from './tokenStorage';
|
|
1230
|
+
import { useNavigate } from 'react-router-dom';
|
|
1231
|
+
|
|
1232
|
+
function App() {
|
|
1233
|
+
const navigate = useNavigate();
|
|
1234
|
+
|
|
1235
|
+
const { currentUser, authenticated, signIn, signOut } = useSecurity(tokenStorage, {
|
|
1236
|
+
onSignIn: () => {
|
|
1237
|
+
// 登录成功后重定向到仪表板
|
|
1238
|
+
navigate('/dashboard');
|
|
1239
|
+
},
|
|
1240
|
+
onSignOut: () => {
|
|
1241
|
+
// 登出后重定向到登录页面
|
|
1242
|
+
navigate('/login');
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
const handleSignIn = async () => {
|
|
1247
|
+
// 直接令牌
|
|
1248
|
+
await signIn(compositeToken);
|
|
1249
|
+
|
|
1250
|
+
// 或异步函数
|
|
1251
|
+
await signIn(async () => {
|
|
1252
|
+
const response = await fetch('/api/auth/login', {
|
|
1253
|
+
method: 'POST',
|
|
1254
|
+
body: JSON.stringify({ username, password })
|
|
1255
|
+
});
|
|
1256
|
+
return response.json();
|
|
1257
|
+
});
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
if (!authenticated) {
|
|
1261
|
+
return <button onClick={handleSignIn}>登录</button>;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
return (
|
|
1265
|
+
<div>
|
|
1266
|
+
<p>欢迎, {currentUser.sub}!</p>
|
|
1267
|
+
<button onClick={signOut}>登出</button>
|
|
1268
|
+
</div>
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
**关键特性:**
|
|
1274
|
+
|
|
1275
|
+
- **响应式认证状态**: 当令牌更改时自动更新
|
|
1276
|
+
- **灵活的登录方法**: 支持直接令牌和异步令牌提供者
|
|
1277
|
+
- **生命周期回调**: 可配置的登录和登出事件回调
|
|
1278
|
+
- **类型安全**: 完全支持 TypeScript,具有 CoSec JWT 负载类型
|
|
1279
|
+
- **令牌持久化**: 与 TokenStorage 集成以实现跨会话持久化
|
|
1280
|
+
|
|
1281
|
+
#### SecurityProvider
|
|
1282
|
+
|
|
1283
|
+
`SecurityProvider` 组件包装您的应用程序以通过 React 上下文提供认证上下文。它在内部使用 `useSecurity` hook,并通过 `useSecurityContext` hook 使认证状态可用于所有子组件。
|
|
1284
|
+
|
|
1285
|
+
```tsx
|
|
1286
|
+
import { SecurityProvider } from '@ahoo-wang/fetcher-react';
|
|
1287
|
+
import { tokenStorage } from './tokenStorage';
|
|
1288
|
+
import { useNavigate } from 'react-router-dom';
|
|
1289
|
+
|
|
1290
|
+
function App() {
|
|
1291
|
+
const navigate = useNavigate();
|
|
1292
|
+
|
|
1293
|
+
return (
|
|
1294
|
+
<SecurityProvider
|
|
1295
|
+
tokenStorage={tokenStorage}
|
|
1296
|
+
onSignIn={() => navigate('/dashboard')}
|
|
1297
|
+
onSignOut={() => navigate('/login')}
|
|
1298
|
+
>
|
|
1299
|
+
<MyApp />
|
|
1300
|
+
</SecurityProvider>
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
**配置选项:**
|
|
1306
|
+
|
|
1307
|
+
- `tokenStorage`: 用于管理认证令牌的 TokenStorage 实例
|
|
1308
|
+
- `onSignIn`: 登录成功时调用的回调函数
|
|
1309
|
+
- `onSignOut`: 登出时调用的回调函数
|
|
1310
|
+
- `children`: 将有权访问安全上下文的子组件
|
|
1311
|
+
|
|
1312
|
+
#### useSecurityContext
|
|
1313
|
+
|
|
1314
|
+
`useSecurityContext` hook 在被 `SecurityProvider` 包装的组件中提供对认证状态和方法的访问。它通过 React 上下文提供与 `useSecurity` 相同的接口。
|
|
1315
|
+
|
|
1316
|
+
```tsx
|
|
1317
|
+
import { useSecurityContext } from '@ahoo-wang/fetcher-react';
|
|
1318
|
+
|
|
1319
|
+
function UserProfile() {
|
|
1320
|
+
const { currentUser, authenticated, signOut } = useSecurityContext();
|
|
1321
|
+
|
|
1322
|
+
if (!authenticated) {
|
|
1323
|
+
return <div>请登录</div>;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
return (
|
|
1327
|
+
<div>
|
|
1328
|
+
<p>欢迎, {currentUser.sub}!</p>
|
|
1329
|
+
<button onClick={signOut}>登出</button>
|
|
1330
|
+
</div>
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
**上下文优势:**
|
|
1336
|
+
|
|
1337
|
+
- **消除属性钻取**: 无需传递属性即可访问认证状态
|
|
1338
|
+
- **组件隔离**: 无论组件树深度如何,组件都可以访问认证状态
|
|
1339
|
+
- **集中式状态**: 应用程序中认证的单一真实来源
|
|
1340
|
+
- **自动重新渲染**: 当认证状态更改时,组件自动重新渲染
|
|
1341
|
+
|
|
1342
|
+
### Wow 查询 Hooks
|
|
378
1343
|
|
|
379
1344
|
Wow 查询 Hooks 提供高级数据查询功能,具有内置的状态管理,用于条件、投影、排序、分页和限制。这些 hooks 专为与 `@ahoo-wang/fetcher-wow` 包配合使用而设计,用于复杂的查询操作。
|
|
380
1345
|
|
|
381
|
-
|
|
1346
|
+
#### 基础查询 Hooks
|
|
382
1347
|
|
|
383
|
-
|
|
1348
|
+
##### useListQuery
|
|
384
1349
|
|
|
385
1350
|
`useListQuery` hook 管理列表查询,具有条件、投影、排序和限制的状态管理。
|
|
386
1351
|
|
|
@@ -391,7 +1356,7 @@ const MyComponent = () => {
|
|
|
391
1356
|
const { result, loading, error, execute, setCondition, setLimit } = useListQuery({
|
|
392
1357
|
initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
|
|
393
1358
|
execute: async (listQuery) => {
|
|
394
|
-
//
|
|
1359
|
+
// 您的列表获取逻辑
|
|
395
1360
|
return fetchListData(listQuery);
|
|
396
1361
|
},
|
|
397
1362
|
});
|
|
@@ -417,7 +1382,37 @@ const MyComponent = () => {
|
|
|
417
1382
|
};
|
|
418
1383
|
```
|
|
419
1384
|
|
|
420
|
-
|
|
1385
|
+
##### 自动执行示例
|
|
1386
|
+
|
|
1387
|
+
```typescript jsx
|
|
1388
|
+
import { useListQuery } from '@ahoo-wang/fetcher-react';
|
|
1389
|
+
|
|
1390
|
+
const MyComponent = () => {
|
|
1391
|
+
const { result, loading, error, execute, setCondition } = useListQuery({
|
|
1392
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
|
|
1393
|
+
execute: async (listQuery) => fetchListData(listQuery),
|
|
1394
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
// 查询将在组件挂载时自动执行
|
|
1398
|
+
// 您仍然可以使用 execute() 手动触发或更新条件
|
|
1399
|
+
|
|
1400
|
+
if (loading) return <div>加载中...</div>;
|
|
1401
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
1402
|
+
|
|
1403
|
+
return (
|
|
1404
|
+
<div>
|
|
1405
|
+
<ul>
|
|
1406
|
+
{result?.map((item, index) => (
|
|
1407
|
+
<li key={index}>{item.name}</li>
|
|
1408
|
+
))}
|
|
1409
|
+
</ul>
|
|
1410
|
+
</div>
|
|
1411
|
+
);
|
|
1412
|
+
};
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
##### usePagedQuery
|
|
421
1416
|
|
|
422
1417
|
`usePagedQuery` hook 管理分页查询,具有条件、投影、分页和排序的状态管理。
|
|
423
1418
|
|
|
@@ -433,7 +1428,7 @@ const MyComponent = () => {
|
|
|
433
1428
|
sort: []
|
|
434
1429
|
},
|
|
435
1430
|
execute: async (pagedQuery) => {
|
|
436
|
-
//
|
|
1431
|
+
// 您的分页获取逻辑
|
|
437
1432
|
return fetchPagedData(pagedQuery);
|
|
438
1433
|
},
|
|
439
1434
|
});
|
|
@@ -448,108 +1443,285 @@ const MyComponent = () => {
|
|
|
448
1443
|
|
|
449
1444
|
return (
|
|
450
1445
|
<div>
|
|
451
|
-
<ul>
|
|
452
|
-
{result?.list?.map((item, index) => (
|
|
453
|
-
<li key={index}>{item.name}</li>
|
|
454
|
-
))}
|
|
455
|
-
</ul>
|
|
456
|
-
<button onClick={() => handlePageChange(result?.pagination?.index! - 1)} disabled={result?.pagination?.index === 1}>
|
|
457
|
-
上一页
|
|
458
|
-
</button>
|
|
459
|
-
<button onClick={() => handlePageChange(result?.pagination?.index! + 1)}>
|
|
460
|
-
下一页
|
|
461
|
-
</button>
|
|
1446
|
+
<ul>
|
|
1447
|
+
{result?.list?.map((item, index) => (
|
|
1448
|
+
<li key={index}>{item.name}</li>
|
|
1449
|
+
))}
|
|
1450
|
+
</ul>
|
|
1451
|
+
<button onClick={() => handlePageChange(result?.pagination?.index! - 1)} disabled={result?.pagination?.index === 1}>
|
|
1452
|
+
上一页
|
|
1453
|
+
</button>
|
|
1454
|
+
<button onClick={() => handlePageChange(result?.pagination?.index! + 1)}>
|
|
1455
|
+
下一页
|
|
1456
|
+
</button>
|
|
1457
|
+
</div>
|
|
1458
|
+
);
|
|
1459
|
+
};
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1462
|
+
###### 自动执行示例
|
|
1463
|
+
|
|
1464
|
+
```typescript jsx
|
|
1465
|
+
import { usePagedQuery } from '@ahoo-wang/fetcher-react';
|
|
1466
|
+
|
|
1467
|
+
const MyComponent = () => {
|
|
1468
|
+
const { result, loading, error, execute, setCondition, setPagination } = usePagedQuery({
|
|
1469
|
+
initialQuery: {
|
|
1470
|
+
condition: {},
|
|
1471
|
+
pagination: { index: 1, size: 10 },
|
|
1472
|
+
projection: {},
|
|
1473
|
+
sort: []
|
|
1474
|
+
},
|
|
1475
|
+
execute: async (pagedQuery) => fetchPagedData(pagedQuery),
|
|
1476
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// 查询将在组件挂载时自动执行
|
|
1480
|
+
|
|
1481
|
+
if (loading) return <div>加载中...</div>;
|
|
1482
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
1483
|
+
|
|
1484
|
+
return (
|
|
1485
|
+
<div>
|
|
1486
|
+
<ul>
|
|
1487
|
+
{result?.list?.map((item, index) => (
|
|
1488
|
+
<li key={index}>{item.name}</li>
|
|
1489
|
+
))}
|
|
1490
|
+
</ul>
|
|
1491
|
+
<button onClick={() => setPagination({ index: result?.pagination?.index! - 1, size: 10 })} disabled={result?.pagination?.index === 1}>
|
|
1492
|
+
上一页
|
|
1493
|
+
</button>
|
|
1494
|
+
<button onClick={() => setPagination({ index: result?.pagination?.index! + 1, size: 10 })}>
|
|
1495
|
+
下一页
|
|
1496
|
+
</button>
|
|
1497
|
+
</div>
|
|
1498
|
+
);
|
|
1499
|
+
};
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
##### useSingleQuery
|
|
1503
|
+
|
|
1504
|
+
`useSingleQuery` hook 管理单个查询,具有条件、投影和排序的状态管理。
|
|
1505
|
+
|
|
1506
|
+
```typescript jsx
|
|
1507
|
+
import { useSingleQuery } from '@ahoo-wang/fetcher-react';
|
|
1508
|
+
|
|
1509
|
+
const MyComponent = () => {
|
|
1510
|
+
const { result, loading, error, execute, setCondition } = useSingleQuery({
|
|
1511
|
+
initialQuery: { condition: {}, projection: {}, sort: [] },
|
|
1512
|
+
execute: async (singleQuery) => {
|
|
1513
|
+
// 您的单个获取逻辑
|
|
1514
|
+
return fetchSingleData(singleQuery);
|
|
1515
|
+
},
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
const handleFetchUser = (userId: string) => {
|
|
1519
|
+
setCondition({ id: userId });
|
|
1520
|
+
execute();
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
if (loading) return <div>加载中...</div>;
|
|
1524
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
1525
|
+
|
|
1526
|
+
return (
|
|
1527
|
+
<div>
|
|
1528
|
+
<button onClick={() => handleFetchUser('123')}>获取用户</button>
|
|
1529
|
+
{result && <p>用户: {result.name}</p>}
|
|
1530
|
+
</div>
|
|
1531
|
+
);
|
|
1532
|
+
};
|
|
1533
|
+
```
|
|
1534
|
+
|
|
1535
|
+
###### 自动执行示例
|
|
1536
|
+
|
|
1537
|
+
```typescript jsx
|
|
1538
|
+
import { useSingleQuery } from '@ahoo-wang/fetcher-react';
|
|
1539
|
+
|
|
1540
|
+
const MyComponent = () => {
|
|
1541
|
+
const { result, loading, error, execute, setCondition } = useSingleQuery({
|
|
1542
|
+
initialQuery: { condition: {}, projection: {}, sort: [] },
|
|
1543
|
+
execute: async (singleQuery) => fetchSingleData(singleQuery),
|
|
1544
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// 查询将在组件挂载时自动执行
|
|
1548
|
+
|
|
1549
|
+
if (loading) return <div>加载中...</div>;
|
|
1550
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
1551
|
+
|
|
1552
|
+
return (
|
|
1553
|
+
<div>
|
|
1554
|
+
{result && <p>用户: {result.name}</p>}
|
|
1555
|
+
</div>
|
|
1556
|
+
);
|
|
1557
|
+
};
|
|
1558
|
+
```
|
|
1559
|
+
|
|
1560
|
+
##### useCountQuery
|
|
1561
|
+
|
|
1562
|
+
`useCountQuery` hook 管理计数查询,具有条件的状态管理。
|
|
1563
|
+
|
|
1564
|
+
```typescript jsx
|
|
1565
|
+
import { useCountQuery } from '@ahoo-wang/fetcher-react';
|
|
1566
|
+
|
|
1567
|
+
const MyComponent = () => {
|
|
1568
|
+
const { result, loading, error, execute, setCondition } = useCountQuery({
|
|
1569
|
+
initialQuery: {},
|
|
1570
|
+
execute: async (condition) => {
|
|
1571
|
+
// 您的计数获取逻辑
|
|
1572
|
+
return fetchCount(condition);
|
|
1573
|
+
},
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
const handleCountActive = () => {
|
|
1577
|
+
setCondition({ status: 'active' });
|
|
1578
|
+
execute();
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
if (loading) return <div>加载中...</div>;
|
|
1582
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
1583
|
+
|
|
1584
|
+
return (
|
|
1585
|
+
<div>
|
|
1586
|
+
<button onClick={handleCountActive}>计数活跃项目</button>
|
|
1587
|
+
<p>总数: {result}</p>
|
|
1588
|
+
</div>
|
|
1589
|
+
);
|
|
1590
|
+
};
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
###### 自动执行示例
|
|
1594
|
+
|
|
1595
|
+
```typescript jsx
|
|
1596
|
+
import { useCountQuery } from '@ahoo-wang/fetcher-react';
|
|
1597
|
+
|
|
1598
|
+
const MyComponent = () => {
|
|
1599
|
+
const { result, loading, error, execute, setCondition } = useCountQuery({
|
|
1600
|
+
initialQuery: {},
|
|
1601
|
+
execute: async (condition) => fetchCount(condition),
|
|
1602
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
// 查询将在组件挂载时自动执行
|
|
1606
|
+
|
|
1607
|
+
if (loading) return <div>加载中...</div>;
|
|
1608
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
1609
|
+
|
|
1610
|
+
return (
|
|
1611
|
+
<div>
|
|
1612
|
+
<p>总数: {result}</p>
|
|
462
1613
|
</div>
|
|
463
1614
|
);
|
|
464
1615
|
};
|
|
465
1616
|
```
|
|
466
1617
|
|
|
467
|
-
|
|
1618
|
+
##### useListStreamQuery
|
|
468
1619
|
|
|
469
|
-
`
|
|
1620
|
+
`useListStreamQuery` hook 管理列表流查询,返回服务器发送事件的 readable stream。
|
|
470
1621
|
|
|
471
1622
|
```typescript jsx
|
|
472
|
-
import {
|
|
1623
|
+
import { useListStreamQuery } from '@ahoo-wang/fetcher-react';
|
|
473
1624
|
|
|
474
1625
|
const MyComponent = () => {
|
|
475
|
-
const { result, loading, error, execute, setCondition } =
|
|
476
|
-
initialQuery: { condition: {}, projection: {}, sort: [] },
|
|
477
|
-
execute: async (
|
|
478
|
-
//
|
|
479
|
-
return
|
|
1626
|
+
const { result, loading, error, execute, setCondition } = useListStreamQuery({
|
|
1627
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 100 },
|
|
1628
|
+
execute: async (listQuery) => {
|
|
1629
|
+
// 您的流获取逻辑
|
|
1630
|
+
return fetchListStream(listQuery);
|
|
480
1631
|
},
|
|
481
1632
|
});
|
|
482
1633
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
1634
|
+
useEffect(() => {
|
|
1635
|
+
if (result) {
|
|
1636
|
+
const reader = result.getReader();
|
|
1637
|
+
const readStream = async () => {
|
|
1638
|
+
try {
|
|
1639
|
+
while (true) {
|
|
1640
|
+
const { done, value } = await reader.read();
|
|
1641
|
+
if (done) break;
|
|
1642
|
+
console.log('接收到:', value);
|
|
1643
|
+
// 处理流事件
|
|
1644
|
+
}
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
console.error('流错误:', error);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
readStream();
|
|
1650
|
+
}
|
|
1651
|
+
}, [result]);
|
|
487
1652
|
|
|
488
1653
|
if (loading) return <div>加载中...</div>;
|
|
489
1654
|
if (error) return <div>错误: {error.message}</div>;
|
|
490
1655
|
|
|
491
1656
|
return (
|
|
492
1657
|
<div>
|
|
493
|
-
<button onClick={
|
|
494
|
-
{result && <p>用户: {result.name}</p>}
|
|
1658
|
+
<button onClick={execute}>开始流</button>
|
|
495
1659
|
</div>
|
|
496
1660
|
);
|
|
497
1661
|
};
|
|
498
1662
|
```
|
|
499
1663
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
`useCountQuery` hook 管理计数查询,具有条件的状态管理。
|
|
1664
|
+
###### 自动执行示例
|
|
503
1665
|
|
|
504
1666
|
```typescript jsx
|
|
505
|
-
import {
|
|
1667
|
+
import { useListStreamQuery } from '@ahoo-wang/fetcher-react';
|
|
506
1668
|
|
|
507
1669
|
const MyComponent = () => {
|
|
508
|
-
const { result, loading, error, execute, setCondition } =
|
|
509
|
-
initialQuery: {},
|
|
510
|
-
execute: async (
|
|
511
|
-
|
|
512
|
-
return fetchCount(condition);
|
|
513
|
-
},
|
|
1670
|
+
const { result, loading, error, execute, setCondition } = useListStreamQuery({
|
|
1671
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 100 },
|
|
1672
|
+
execute: async (listQuery) => fetchListStream(listQuery),
|
|
1673
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
514
1674
|
});
|
|
515
1675
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1676
|
+
useEffect(() => {
|
|
1677
|
+
if (result) {
|
|
1678
|
+
const reader = result.getReader();
|
|
1679
|
+
const readStream = async () => {
|
|
1680
|
+
try {
|
|
1681
|
+
while (true) {
|
|
1682
|
+
const { done, value } = await reader.read();
|
|
1683
|
+
if (done) break;
|
|
1684
|
+
console.log('接收到:', value);
|
|
1685
|
+
// 处理流事件
|
|
1686
|
+
}
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
console.error('流错误:', error);
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
readStream();
|
|
1692
|
+
}
|
|
1693
|
+
}, [result]);
|
|
1694
|
+
|
|
1695
|
+
// 流将在组件挂载时自动启动
|
|
520
1696
|
|
|
521
1697
|
if (loading) return <div>加载中...</div>;
|
|
522
1698
|
if (error) return <div>错误: {error.message}</div>;
|
|
523
1699
|
|
|
524
1700
|
return (
|
|
525
1701
|
<div>
|
|
526
|
-
|
|
527
|
-
<p>总数: {result}</p>
|
|
1702
|
+
{/* 流已自动启动 */}
|
|
528
1703
|
</div>
|
|
529
1704
|
);
|
|
530
1705
|
};
|
|
531
1706
|
```
|
|
532
1707
|
|
|
533
|
-
|
|
1708
|
+
#### Fetcher 查询 Hooks
|
|
534
1709
|
|
|
535
|
-
|
|
1710
|
+
##### useFetcherCountQuery
|
|
536
1711
|
|
|
537
1712
|
`useFetcherCountQuery` hook 是使用 Fetcher 库执行计数查询的专用 React hook。它专为需要检索匹配特定条件的记录数量的场景而设计,返回表示计数的数字。
|
|
538
1713
|
|
|
539
1714
|
```typescript jsx
|
|
540
1715
|
import { useFetcherCountQuery } from '@ahoo-wang/fetcher-react';
|
|
541
1716
|
import { all } from '@ahoo-wang/fetcher-wow';
|
|
542
|
-
|
|
543
1717
|
function UserCountComponent() {
|
|
544
1718
|
const { data: count, loading, error, execute } = useFetcherCountQuery({
|
|
545
1719
|
url: '/api/users/count',
|
|
546
1720
|
initialQuery: all(),
|
|
547
1721
|
autoExecute: true,
|
|
548
1722
|
});
|
|
549
|
-
|
|
550
1723
|
if (loading) return <div>加载中...</div>;
|
|
551
1724
|
if (error) return <div>错误: {error.message}</div>;
|
|
552
|
-
|
|
553
1725
|
return (
|
|
554
1726
|
<div>
|
|
555
1727
|
<div>活跃用户总数: {count}</div>
|
|
@@ -559,23 +1731,19 @@ function UserCountComponent() {
|
|
|
559
1731
|
}
|
|
560
1732
|
```
|
|
561
1733
|
|
|
562
|
-
|
|
1734
|
+
###### 自动执行示例
|
|
563
1735
|
|
|
564
1736
|
```typescript jsx
|
|
565
1737
|
import { useFetcherCountQuery } from '@ahoo-wang/fetcher-react';
|
|
566
|
-
|
|
567
1738
|
const MyComponent = () => {
|
|
568
1739
|
const { data: count, loading, error, execute } = useFetcherCountQuery({
|
|
569
1740
|
url: '/api/users/count',
|
|
570
1741
|
initialQuery: { status: 'active' },
|
|
571
1742
|
autoExecute: true, // 组件挂载时自动执行
|
|
572
1743
|
});
|
|
573
|
-
|
|
574
1744
|
// 查询将在组件挂载时自动执行
|
|
575
|
-
|
|
576
1745
|
if (loading) return <div>加载中...</div>;
|
|
577
1746
|
if (error) return <div>错误: {error.message}</div>;
|
|
578
|
-
|
|
579
1747
|
return (
|
|
580
1748
|
<div>
|
|
581
1749
|
<p>活跃用户总数: {count}</p>
|
|
@@ -584,7 +1752,7 @@ const MyComponent = () => {
|
|
|
584
1752
|
};
|
|
585
1753
|
```
|
|
586
1754
|
|
|
587
|
-
|
|
1755
|
+
##### useFetcherPagedQuery
|
|
588
1756
|
|
|
589
1757
|
`useFetcherPagedQuery` hook 是使用 Fetcher 库执行分页查询的专用 React hook。它专为需要检索匹配查询条件的分页数据的场景而设计,返回包含当前页面项目以及分页元数据的 PagedList。
|
|
590
1758
|
|
|
@@ -653,7 +1821,7 @@ function UserListComponent() {
|
|
|
653
1821
|
}
|
|
654
1822
|
```
|
|
655
1823
|
|
|
656
|
-
|
|
1824
|
+
###### 自动执行示例
|
|
657
1825
|
|
|
658
1826
|
```typescript jsx
|
|
659
1827
|
import { useFetcherPagedQuery } from '@ahoo-wang/fetcher-react';
|
|
@@ -689,7 +1857,7 @@ const MyComponent = () => {
|
|
|
689
1857
|
};
|
|
690
1858
|
```
|
|
691
1859
|
|
|
692
|
-
|
|
1860
|
+
##### useFetcherListQuery
|
|
693
1861
|
|
|
694
1862
|
`useFetcherListQuery` hook 是使用 Fetcher 库执行列表查询的专用 React hook。它专为获取项目列表而设计,支持通过 ListQuery 类型进行过滤、排序和分页,返回结果数组。
|
|
695
1863
|
|
|
@@ -750,7 +1918,7 @@ function UserListComponent() {
|
|
|
750
1918
|
}
|
|
751
1919
|
```
|
|
752
1920
|
|
|
753
|
-
|
|
1921
|
+
###### 自动执行示例
|
|
754
1922
|
|
|
755
1923
|
```typescript jsx
|
|
756
1924
|
import { useFetcherListQuery } from '@ahoo-wang/fetcher-react';
|
|
@@ -785,7 +1953,7 @@ const MyComponent = () => {
|
|
|
785
1953
|
};
|
|
786
1954
|
```
|
|
787
1955
|
|
|
788
|
-
|
|
1956
|
+
##### useFetcherListStreamQuery
|
|
789
1957
|
|
|
790
1958
|
`useFetcherListStreamQuery` hook 是使用 Fetcher 库通过服务器发送事件执行列表流查询的专用 React hook。它专为需要检索匹配列表查询条件的数据流场景而设计,返回 JSON 服务器发送事件的 ReadableStream,用于实时数据流式传输。
|
|
791
1959
|
|
|
@@ -848,7 +2016,7 @@ function UserStreamComponent() {
|
|
|
848
2016
|
}
|
|
849
2017
|
```
|
|
850
2018
|
|
|
851
|
-
|
|
2019
|
+
###### 自动执行示例
|
|
852
2020
|
|
|
853
2021
|
```typescript jsx
|
|
854
2022
|
import { useFetcherListStreamQuery } from '@ahoo-wang/fetcher-react';
|
|
@@ -896,163 +2064,587 @@ const MyComponent = () => {
|
|
|
896
2064
|
if (error) return <div>错误: {error.message}</div>;
|
|
897
2065
|
|
|
898
2066
|
return (
|
|
899
|
-
<div>
|
|
900
|
-
<h2>实时通知</h2>
|
|
901
|
-
<div ref={notificationsRef}></div>
|
|
902
|
-
</div>
|
|
2067
|
+
<div>
|
|
2068
|
+
<h2>实时通知</h2>
|
|
2069
|
+
<div ref={notificationsRef}></div>
|
|
2070
|
+
</div>
|
|
2071
|
+
);
|
|
2072
|
+
};
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
##### useFetcherSingleQuery
|
|
2076
|
+
|
|
2077
|
+
`useFetcherSingleQuery` hook 是使用 Fetcher 库执行单个项目查询的专用 React hook。它专为获取单个项目而设计,支持通过 SingleQuery 类型进行过滤和排序,返回单个结果项目。
|
|
2078
|
+
|
|
2079
|
+
```typescript jsx
|
|
2080
|
+
import { useFetcherSingleQuery } from '@ahoo-wang/fetcher-react';
|
|
2081
|
+
import { singleQuery, eq } from '@ahoo-wang/fetcher-wow';
|
|
2082
|
+
|
|
2083
|
+
interface User {
|
|
2084
|
+
id: string;
|
|
2085
|
+
name: string;
|
|
2086
|
+
email: string;
|
|
2087
|
+
createdAt: string;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function UserProfileComponent({ userId }: { userId: string }) {
|
|
2091
|
+
const {
|
|
2092
|
+
loading,
|
|
2093
|
+
result: user,
|
|
2094
|
+
error,
|
|
2095
|
+
execute,
|
|
2096
|
+
} = useFetcherSingleQuery<User, keyof User>({
|
|
2097
|
+
url: `/api/users/${userId}`,
|
|
2098
|
+
initialQuery: singleQuery({
|
|
2099
|
+
condition: eq('id', userId),
|
|
2100
|
+
}),
|
|
2101
|
+
autoExecute: true,
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
if (loading) return <div>正在加载用户...</div>;
|
|
2105
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
2106
|
+
if (!user) return <div>未找到用户</div>;
|
|
2107
|
+
|
|
2108
|
+
return (
|
|
2109
|
+
<div>
|
|
2110
|
+
<h2>{user.name}</h2>
|
|
2111
|
+
<p>邮箱: {user.email}</p>
|
|
2112
|
+
<p>创建时间: {user.createdAt}</p>
|
|
2113
|
+
<button onClick={execute}>刷新</button>
|
|
2114
|
+
</div>
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
###### 自动执行示例
|
|
2120
|
+
|
|
2121
|
+
```typescript jsx
|
|
2122
|
+
import { useFetcherSingleQuery } from '@ahoo-wang/fetcher-react';
|
|
2123
|
+
|
|
2124
|
+
const MyComponent = () => {
|
|
2125
|
+
const { result: product, loading, error, execute } = useFetcherSingleQuery({
|
|
2126
|
+
url: '/api/products/featured',
|
|
2127
|
+
initialQuery: {
|
|
2128
|
+
condition: { featured: true },
|
|
2129
|
+
projection: {},
|
|
2130
|
+
sort: []
|
|
2131
|
+
},
|
|
2132
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
// 查询将在组件挂载时自动执行
|
|
2136
|
+
|
|
2137
|
+
if (loading) return <div>加载中...</div>;
|
|
2138
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
2139
|
+
if (!product) return <div>未找到产品</div>;
|
|
2140
|
+
|
|
2141
|
+
return (
|
|
2142
|
+
<div>
|
|
2143
|
+
<h2>特色产品</h2>
|
|
2144
|
+
<div>{product.name}</div>
|
|
2145
|
+
<div>{product.description}</div>
|
|
2146
|
+
</div>
|
|
2147
|
+
);
|
|
2148
|
+
};
|
|
2149
|
+
```
|
|
2150
|
+
|
|
2151
|
+
## 最佳实践
|
|
2152
|
+
|
|
2153
|
+
### 性能优化
|
|
2154
|
+
|
|
2155
|
+
- 使用 `autoExecute: false` 来控制查询的执行时机
|
|
2156
|
+
- 当启用 `autoExecute` 时,使用 `setQuery` 更新查询以触发自动重新执行
|
|
2157
|
+
- 在 `execute` 函数中记忆化昂贵的计算
|
|
2158
|
+
|
|
2159
|
+
### 错误处理
|
|
2160
|
+
|
|
2161
|
+
- 始终在组件中处理加载和错误状态
|
|
2162
|
+
- 使用自定义错误类型以更好地分类错误
|
|
2163
|
+
- 为瞬时故障实现重试逻辑
|
|
2164
|
+
|
|
2165
|
+
### 类型安全
|
|
2166
|
+
|
|
2167
|
+
- 为查询参数和结果定义严格的接口
|
|
2168
|
+
- 在整个应用程序中一致使用泛型类型
|
|
2169
|
+
- 启用严格 TypeScript 模式以获得最大安全性
|
|
2170
|
+
|
|
2171
|
+
### 状态管理
|
|
2172
|
+
|
|
2173
|
+
- 与全局状态管理结合使用(Redux、Zustand)以处理复杂应用
|
|
2174
|
+
- 使用 `useKeyStorage` 进行持久化的客户端数据存储
|
|
2175
|
+
- 实现乐观更新以改善用户体验
|
|
2176
|
+
|
|
2177
|
+
## 🚀 高级使用示例
|
|
2178
|
+
|
|
2179
|
+
### 自定义 Hook 组合
|
|
2180
|
+
|
|
2181
|
+
通过组合多个 fetcher-react hooks 创建可重用的 hooks:
|
|
2182
|
+
|
|
2183
|
+
```typescript jsx
|
|
2184
|
+
import { useFetcher, usePromiseState, useLatest } from '@ahoo-wang/fetcher-react';
|
|
2185
|
+
import { useCallback, useEffect } from 'react';
|
|
2186
|
+
|
|
2187
|
+
function useUserProfile(userId: string) {
|
|
2188
|
+
const latestUserId = useLatest(userId);
|
|
2189
|
+
const { loading, result: profile, error, execute } = useFetcher();
|
|
2190
|
+
|
|
2191
|
+
const fetchProfile = useCallback(() => {
|
|
2192
|
+
execute({
|
|
2193
|
+
url: `/api/users/${latestUserId.current}`,
|
|
2194
|
+
method: 'GET'
|
|
2195
|
+
});
|
|
2196
|
+
}, [execute, latestUserId]);
|
|
2197
|
+
|
|
2198
|
+
useEffect(() => {
|
|
2199
|
+
if (userId) {
|
|
2200
|
+
fetchProfile();
|
|
2201
|
+
}
|
|
2202
|
+
}, [userId, fetchProfile]);
|
|
2203
|
+
|
|
2204
|
+
return { profile, loading, error, refetch: fetchProfile };
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// 使用
|
|
2208
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
2209
|
+
const { profile, loading, error, refetch } = useUserProfile(userId);
|
|
2210
|
+
|
|
2211
|
+
if (loading) return <div>加载中...</div>;
|
|
2212
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
2213
|
+
|
|
2214
|
+
return (
|
|
2215
|
+
<div>
|
|
2216
|
+
<h2>{profile?.name}</h2>
|
|
2217
|
+
<button onClick={refetch}>刷新</button>
|
|
2218
|
+
</div>
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
```
|
|
2222
|
+
|
|
2223
|
+
### 错误边界集成
|
|
2224
|
+
|
|
2225
|
+
与 React 错误边界集成以获得更好的错误处理:
|
|
2226
|
+
|
|
2227
|
+
```typescript jsx
|
|
2228
|
+
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
2229
|
+
|
|
2230
|
+
class FetchErrorBoundary extends Component<
|
|
2231
|
+
{ children: ReactNode; fallback?: ReactNode },
|
|
2232
|
+
{ hasError: boolean; error?: Error }
|
|
2233
|
+
> {
|
|
2234
|
+
constructor(props: { children: ReactNode; fallback?: ReactNode }) {
|
|
2235
|
+
super(props);
|
|
2236
|
+
this.state = { hasError: false };
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
static getDerivedStateFromError(error: Error) {
|
|
2240
|
+
return { hasError: true, error };
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
2244
|
+
console.error('获取错误边界捕获到错误:', error, errorInfo);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
render() {
|
|
2248
|
+
if (this.state.hasError) {
|
|
2249
|
+
return this.props.fallback || <div>出现问题。</div>;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
return this.props.children;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// 与 hooks 一起使用
|
|
2257
|
+
function DataComponent() {
|
|
2258
|
+
const { result, loading, error, execute } = useFetcher();
|
|
2259
|
+
|
|
2260
|
+
// 错误将被边界捕获(如果抛出)
|
|
2261
|
+
if (error) {
|
|
2262
|
+
throw error;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
return (
|
|
2266
|
+
<div>
|
|
2267
|
+
{loading ? '加载中...' : JSON.stringify(result)}
|
|
2268
|
+
</div>
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// 包装使用 fetcher hooks 的组件
|
|
2273
|
+
function App() {
|
|
2274
|
+
return (
|
|
2275
|
+
<FetchErrorBoundary fallback={<div>加载数据失败</div>}>
|
|
2276
|
+
<DataComponent />
|
|
2277
|
+
</FetchErrorBoundary>
|
|
903
2278
|
);
|
|
904
|
-
}
|
|
2279
|
+
}
|
|
905
2280
|
```
|
|
906
2281
|
|
|
907
|
-
###
|
|
2282
|
+
### Suspense 集成
|
|
908
2283
|
|
|
909
|
-
|
|
2284
|
+
与 React Suspense 一起使用以获得更好的加载状态:
|
|
910
2285
|
|
|
911
2286
|
```typescript jsx
|
|
912
|
-
import {
|
|
913
|
-
import {
|
|
2287
|
+
import { Suspense, useState } from 'react';
|
|
2288
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
914
2289
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
2290
|
+
// 创建抛出 Promise 的资源
|
|
2291
|
+
function createDataResource<T>(promise: Promise<T>) {
|
|
2292
|
+
let status = 'pending';
|
|
2293
|
+
let result: T;
|
|
2294
|
+
let error: Error;
|
|
2295
|
+
|
|
2296
|
+
const suspender = promise.then(
|
|
2297
|
+
(data) => {
|
|
2298
|
+
status = 'success';
|
|
2299
|
+
result = data;
|
|
2300
|
+
},
|
|
2301
|
+
(err) => {
|
|
2302
|
+
status = 'error';
|
|
2303
|
+
error = err;
|
|
2304
|
+
}
|
|
2305
|
+
);
|
|
2306
|
+
|
|
2307
|
+
return {
|
|
2308
|
+
read() {
|
|
2309
|
+
if (status === 'pending') {
|
|
2310
|
+
throw suspender;
|
|
2311
|
+
} else if (status === 'error') {
|
|
2312
|
+
throw error;
|
|
2313
|
+
} else {
|
|
2314
|
+
return result;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
920
2318
|
}
|
|
921
2319
|
|
|
922
|
-
function
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
error,
|
|
927
|
-
execute,
|
|
928
|
-
} = useFetcherSingleQuery<User, keyof User>({
|
|
929
|
-
url: `/api/users/${userId}`,
|
|
930
|
-
initialQuery: singleQuery({
|
|
931
|
-
condition: eq('id', userId),
|
|
932
|
-
}),
|
|
933
|
-
autoExecute: true,
|
|
934
|
-
});
|
|
2320
|
+
function DataComponent({ resource }: { resource: any }) {
|
|
2321
|
+
const data = resource.read(); // 如果待处理将抛出
|
|
2322
|
+
return <div>{JSON.stringify(data)}</div>;
|
|
2323
|
+
}
|
|
935
2324
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
2325
|
+
function App() {
|
|
2326
|
+
const [resource, setResource] = useState<any>(null);
|
|
2327
|
+
|
|
2328
|
+
const handleFetch = () => {
|
|
2329
|
+
const { execute } = useFetcher();
|
|
2330
|
+
const promise = execute({ url: '/api/data', method: 'GET' });
|
|
2331
|
+
setResource(createDataResource(promise));
|
|
2332
|
+
};
|
|
939
2333
|
|
|
940
2334
|
return (
|
|
941
2335
|
<div>
|
|
942
|
-
<
|
|
943
|
-
<
|
|
944
|
-
|
|
945
|
-
|
|
2336
|
+
<button onClick={handleFetch}>获取数据</button>
|
|
2337
|
+
<Suspense fallback={<div>加载中...</div>}>
|
|
2338
|
+
{resource && <DataComponent resource={resource} />}
|
|
2339
|
+
</Suspense>
|
|
946
2340
|
</div>
|
|
947
2341
|
);
|
|
948
2342
|
}
|
|
949
2343
|
```
|
|
950
2344
|
|
|
951
|
-
|
|
2345
|
+
### 性能优化模式
|
|
2346
|
+
|
|
2347
|
+
优化性能的高级模式:
|
|
952
2348
|
|
|
953
2349
|
```typescript jsx
|
|
954
|
-
import {
|
|
2350
|
+
import { useMemo, useCallback, useRef } from 'react';
|
|
2351
|
+
import { useListQuery } from '@ahoo-wang/fetcher-react';
|
|
955
2352
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
2353
|
+
function OptimizedDataTable({ filters, sortBy }) {
|
|
2354
|
+
// 记忆化查询配置以防止不必要的重新执行
|
|
2355
|
+
const queryConfig = useMemo(() => ({
|
|
2356
|
+
condition: filters,
|
|
2357
|
+
sort: [{ field: sortBy, order: 'asc' }],
|
|
2358
|
+
limit: 50
|
|
2359
|
+
}), [filters, sortBy]);
|
|
2360
|
+
|
|
2361
|
+
const { result, loading, execute, setCondition } = useListQuery({
|
|
2362
|
+
initialQuery: queryConfig,
|
|
2363
|
+
execute: useCallback(async (query) => {
|
|
2364
|
+
// 防抖 API 调用
|
|
2365
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
2366
|
+
return fetchData(query);
|
|
2367
|
+
}, []),
|
|
2368
|
+
autoExecute: true
|
|
965
2369
|
});
|
|
966
2370
|
|
|
967
|
-
//
|
|
2371
|
+
// 使用 ref 跟踪最新过滤器而不引起重新渲染
|
|
2372
|
+
const filtersRef = useRef(filters);
|
|
968
2373
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
2374
|
+
useEffect(() => {
|
|
2375
|
+
filtersRef.current = filters;
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
// 防抖搜索
|
|
2379
|
+
const debouncedSearch = useMemo(
|
|
2380
|
+
() => debounce((searchTerm: string) => {
|
|
2381
|
+
setCondition({ ...filtersRef.current, search: searchTerm });
|
|
2382
|
+
}, 500),
|
|
2383
|
+
[setCondition]
|
|
2384
|
+
);
|
|
972
2385
|
|
|
973
2386
|
return (
|
|
974
2387
|
<div>
|
|
975
|
-
<
|
|
976
|
-
|
|
977
|
-
|
|
2388
|
+
<input
|
|
2389
|
+
onChange={(e) => debouncedSearch(e.target.value)}
|
|
2390
|
+
placeholder="搜索..."
|
|
2391
|
+
/>
|
|
2392
|
+
{loading ? '加载中...' : (
|
|
2393
|
+
<table>
|
|
2394
|
+
<tbody>
|
|
2395
|
+
{result?.map(item => (
|
|
2396
|
+
<tr key={item.id}>
|
|
2397
|
+
<td>{item.name}</td>
|
|
2398
|
+
</tr>
|
|
2399
|
+
))}
|
|
2400
|
+
</tbody>
|
|
2401
|
+
</table>
|
|
2402
|
+
)}
|
|
978
2403
|
</div>
|
|
979
2404
|
);
|
|
980
|
-
}
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// 防抖工具
|
|
2408
|
+
function debounce<T extends (...args: any[]) => any>(
|
|
2409
|
+
func: T,
|
|
2410
|
+
wait: number
|
|
2411
|
+
): (...args: Parameters<T>) => void {
|
|
2412
|
+
let timeout: NodeJS.Timeout;
|
|
2413
|
+
return (...args: Parameters<T>) => {
|
|
2414
|
+
clearTimeout(timeout);
|
|
2415
|
+
timeout = setTimeout(() => func(...args), wait);
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
981
2418
|
```
|
|
982
2419
|
|
|
983
|
-
###
|
|
2420
|
+
### 真实世界集成示例
|
|
984
2421
|
|
|
985
|
-
|
|
2422
|
+
显示与流行库集成的完整示例:
|
|
986
2423
|
|
|
987
|
-
|
|
2424
|
+
#### 与 React Query (TanStack Query) 集成
|
|
988
2425
|
|
|
989
2426
|
```typescript jsx
|
|
990
|
-
import {
|
|
2427
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2428
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
991
2429
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
2430
|
+
function useUserData(userId: string) {
|
|
2431
|
+
return useQuery({
|
|
2432
|
+
queryKey: ['user', userId],
|
|
2433
|
+
queryFn: async () => {
|
|
2434
|
+
const { execute } = useFetcher();
|
|
2435
|
+
const result = await execute({
|
|
2436
|
+
url: `/api/users/${userId}`,
|
|
2437
|
+
method: 'GET'
|
|
2438
|
+
});
|
|
2439
|
+
return result;
|
|
2440
|
+
}
|
|
999
2441
|
});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
2445
|
+
const { data, isLoading, error } = useUserData(userId);
|
|
2446
|
+
|
|
2447
|
+
if (isLoading) return <div>加载中...</div>;
|
|
2448
|
+
if (error) return <div>错误: {error.message}</div>;
|
|
2449
|
+
|
|
2450
|
+
return <div>欢迎, {data.name}!</div>;
|
|
2451
|
+
}
|
|
2452
|
+
```
|
|
2453
|
+
|
|
2454
|
+
#### 与 Redux Toolkit 集成
|
|
2455
|
+
|
|
2456
|
+
```typescript jsx
|
|
2457
|
+
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
|
2458
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
2459
|
+
|
|
2460
|
+
const fetchUserData = createAsyncThunk(
|
|
2461
|
+
'user/fetchData',
|
|
2462
|
+
async (userId: string) => {
|
|
2463
|
+
const { execute } = useFetcher();
|
|
2464
|
+
return await execute({
|
|
2465
|
+
url: `/api/users/${userId}`,
|
|
2466
|
+
method: 'GET'
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
);
|
|
2470
|
+
|
|
2471
|
+
const userSlice = createSlice({
|
|
2472
|
+
name: 'user',
|
|
2473
|
+
initialState: { data: null, loading: false, error: null },
|
|
2474
|
+
reducers: {},
|
|
2475
|
+
extraReducers: (builder) => {
|
|
2476
|
+
builder
|
|
2477
|
+
.addCase(fetchUserData.pending, (state) => {
|
|
2478
|
+
state.loading = true;
|
|
2479
|
+
})
|
|
2480
|
+
.addCase(fetchUserData.fulfilled, (state, action) => {
|
|
2481
|
+
state.loading = false;
|
|
2482
|
+
state.data = action.payload;
|
|
2483
|
+
})
|
|
2484
|
+
.addCase(fetchUserData.rejected, (state, action) => {
|
|
2485
|
+
state.loading = false;
|
|
2486
|
+
state.error = action.error.message;
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
});
|
|
2490
|
+
|
|
2491
|
+
function UserComponent({ userId }: { userId: string }) {
|
|
2492
|
+
const dispatch = useDispatch();
|
|
2493
|
+
const { data, loading, error } = useSelector((state) => state.user);
|
|
1000
2494
|
|
|
1001
2495
|
useEffect(() => {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
2496
|
+
dispatch(fetchUserData(userId));
|
|
2497
|
+
}, [userId, dispatch]);
|
|
2498
|
+
|
|
2499
|
+
if (loading) return <div>加载中...</div>;
|
|
2500
|
+
if (error) return <div>错误: {error}</div>;
|
|
2501
|
+
|
|
2502
|
+
return <div>{data?.name}</div>;
|
|
2503
|
+
}
|
|
2504
|
+
```
|
|
2505
|
+
|
|
2506
|
+
#### 与 Zustand 集成
|
|
2507
|
+
|
|
2508
|
+
```typescript jsx
|
|
2509
|
+
import { create } from 'zustand';
|
|
2510
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
2511
|
+
|
|
2512
|
+
interface UserStore {
|
|
2513
|
+
user: any;
|
|
2514
|
+
loading: boolean;
|
|
2515
|
+
error: string | null;
|
|
2516
|
+
fetchUser: (userId: string) => Promise<void>;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
const useUserStore = create<UserStore>((set) => ({
|
|
2520
|
+
user: null,
|
|
2521
|
+
loading: false,
|
|
2522
|
+
error: null,
|
|
2523
|
+
fetchUser: async (userId) => {
|
|
2524
|
+
set({ loading: true, error: null });
|
|
2525
|
+
try {
|
|
2526
|
+
const { execute } = useFetcher();
|
|
2527
|
+
const user = await execute({
|
|
2528
|
+
url: `/api/users/${userId}`,
|
|
2529
|
+
method: 'GET'
|
|
2530
|
+
});
|
|
2531
|
+
set({ user, loading: false });
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
set({ error: error.message, loading: false });
|
|
1017
2534
|
}
|
|
1018
|
-
}
|
|
2535
|
+
}
|
|
2536
|
+
}));
|
|
2537
|
+
|
|
2538
|
+
function UserComponent({ userId }: { userId: string }) {
|
|
2539
|
+
const { user, loading, error, fetchUser } = useUserStore();
|
|
2540
|
+
|
|
2541
|
+
useEffect(() => {
|
|
2542
|
+
fetchUser(userId);
|
|
2543
|
+
}, [userId, fetchUser]);
|
|
1019
2544
|
|
|
1020
2545
|
if (loading) return <div>加载中...</div>;
|
|
1021
|
-
if (error) return <div>错误: {error
|
|
2546
|
+
if (error) return <div>错误: {error}</div>;
|
|
1022
2547
|
|
|
1023
|
-
return
|
|
1024
|
-
|
|
1025
|
-
<button onClick={execute}>开始流</button>
|
|
1026
|
-
</div>
|
|
1027
|
-
);
|
|
1028
|
-
};
|
|
2548
|
+
return <div>{user?.name}</div>;
|
|
2549
|
+
}
|
|
1029
2550
|
```
|
|
1030
2551
|
|
|
1031
|
-
|
|
2552
|
+
### 测试模式
|
|
1032
2553
|
|
|
1033
|
-
|
|
2554
|
+
Hooks 的综合测试示例:
|
|
1034
2555
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
2556
|
+
```typescript jsx
|
|
2557
|
+
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
2558
|
+
import { useFetcher, useListQuery } from '@ahoo-wang/fetcher-react';
|
|
1038
2559
|
|
|
1039
|
-
|
|
2560
|
+
// 模拟 fetcher
|
|
2561
|
+
jest.mock('@ahoo-wang/fetcher', () => ({
|
|
2562
|
+
Fetcher: jest.fn().mockImplementation(() => ({
|
|
2563
|
+
request: jest.fn(),
|
|
2564
|
+
})),
|
|
2565
|
+
}));
|
|
1040
2566
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
2567
|
+
describe('useFetcher', () => {
|
|
2568
|
+
it('应处理成功的获取', async () => {
|
|
2569
|
+
const mockData = { id: 1, name: 'Test' };
|
|
2570
|
+
const mockFetcher = { request: jest.fn().mockResolvedValue(mockData) };
|
|
1044
2571
|
|
|
1045
|
-
|
|
2572
|
+
const { result } = renderHook(() => useFetcher({ fetcher: mockFetcher }));
|
|
1046
2573
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
2574
|
+
act(() => {
|
|
2575
|
+
result.current.execute({ url: '/api/test', method: 'GET' });
|
|
2576
|
+
});
|
|
1050
2577
|
|
|
1051
|
-
|
|
2578
|
+
await waitFor(() => {
|
|
2579
|
+
expect(result.current.loading).toBe(false);
|
|
2580
|
+
expect(result.current.result).toEqual(mockData);
|
|
2581
|
+
expect(result.current.error).toBe(null);
|
|
2582
|
+
});
|
|
2583
|
+
});
|
|
1052
2584
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
2585
|
+
it('应处理获取错误', async () => {
|
|
2586
|
+
const mockError = new Error('网络错误');
|
|
2587
|
+
const mockFetcher = { request: jest.fn().mockRejectedValue(mockError) };
|
|
2588
|
+
|
|
2589
|
+
const { result } = renderHook(() => useFetcher({ fetcher: mockFetcher }));
|
|
2590
|
+
|
|
2591
|
+
act(() => {
|
|
2592
|
+
result.current.execute({ url: '/api/test', method: 'GET' });
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
await waitFor(() => {
|
|
2596
|
+
expect(result.current.loading).toBe(false);
|
|
2597
|
+
expect(result.current.error).toEqual(mockError);
|
|
2598
|
+
expect(result.current.result).toBe(null);
|
|
2599
|
+
});
|
|
2600
|
+
});
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
describe('useListQuery', () => {
|
|
2604
|
+
it('应管理查询状态', async () => {
|
|
2605
|
+
const mockData = [{ id: 1, name: 'Item 1' }];
|
|
2606
|
+
const mockExecute = jest.fn().mockResolvedValue(mockData);
|
|
2607
|
+
|
|
2608
|
+
const { result } = renderHook(() =>
|
|
2609
|
+
useListQuery({
|
|
2610
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
|
|
2611
|
+
execute: mockExecute,
|
|
2612
|
+
}),
|
|
2613
|
+
);
|
|
2614
|
+
|
|
2615
|
+
act(() => {
|
|
2616
|
+
result.current.execute();
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
await waitFor(() => {
|
|
2620
|
+
expect(result.current.loading).toBe(false);
|
|
2621
|
+
expect(result.current.result).toEqual(mockData);
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
expect(mockExecute).toHaveBeenCalledWith({
|
|
2625
|
+
condition: {},
|
|
2626
|
+
projection: {},
|
|
2627
|
+
sort: [],
|
|
2628
|
+
limit: 10,
|
|
2629
|
+
});
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
it('应更新条件', () => {
|
|
2633
|
+
const { result } = renderHook(() =>
|
|
2634
|
+
useListQuery({
|
|
2635
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
|
|
2636
|
+
execute: jest.fn(),
|
|
2637
|
+
}),
|
|
2638
|
+
);
|
|
2639
|
+
|
|
2640
|
+
act(() => {
|
|
2641
|
+
result.current.setCondition({ status: 'active' });
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
expect(result.current.condition).toEqual({ status: 'active' });
|
|
2645
|
+
});
|
|
2646
|
+
});
|
|
2647
|
+
```
|
|
1056
2648
|
|
|
1057
2649
|
## API 参考
|
|
1058
2650
|
|
|
@@ -1067,7 +2659,7 @@ function useDebouncedCallback<T extends (...args: any[]) => any>(
|
|
|
1067
2659
|
): UseDebouncedCallbackReturn<T>;
|
|
1068
2660
|
```
|
|
1069
2661
|
|
|
1070
|
-
|
|
2662
|
+
为回调函数提供防抖版本的 React hook,支持前缘/后缘执行选项。
|
|
1071
2663
|
|
|
1072
2664
|
**类型参数:**
|
|
1073
2665
|
|
|
@@ -1146,104 +2738,26 @@ function useDebouncedFetcher<R, E = FetcherError>(
|
|
|
1146
2738
|
|
|
1147
2739
|
**返回:**
|
|
1148
2740
|
|
|
1149
|
-
包含以下内容的对象:
|
|
1150
|
-
|
|
1151
|
-
- `loading`: 布尔值,表示获取当前是否正在执行
|
|
1152
|
-
- `result`: 获取的解析值
|
|
1153
|
-
- `error`: 执行期间发生的任何错误
|
|
1154
|
-
- `status`: 当前执行状态
|
|
1155
|
-
- `exchange`: 表示正在进行的获取操作的 FetchExchange 对象
|
|
1156
|
-
- `run`: 使用请求参数执行防抖获取的函数
|
|
1157
|
-
- `cancel`: 取消任何待处理防抖执行的函数
|
|
1158
|
-
- `isPending`: 布尔值,表示防抖调用是否待处理
|
|
1159
|
-
|
|
1160
|
-
#### useDebouncedFetcherQuery
|
|
1161
|
-
|
|
1162
|
-
```typescript
|
|
1163
|
-
function useDebouncedFetcherQuery<Q, R, E = FetcherError>(
|
|
1164
|
-
options: UseDebouncedFetcherQueryOptions<Q, R, E>,
|
|
1165
|
-
): UseDebouncedFetcherQueryReturn<Q, R, E>;
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
将基于查询的 HTTP 获取与防抖相结合,非常适合搜索输入和动态查询场景,您希望根据查询参数防抖 API 调用。
|
|
1169
|
-
|
|
1170
|
-
```typescript jsx
|
|
1171
|
-
import { useDebouncedFetcherQuery } from '@ahoo-wang/fetcher-react';
|
|
1172
|
-
|
|
1173
|
-
interface SearchQuery {
|
|
1174
|
-
keyword: string;
|
|
1175
|
-
limit: number;
|
|
1176
|
-
filters?: { category?: string };
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
interface SearchResult {
|
|
1180
|
-
items: Array<{ id: string; title: string }>;
|
|
1181
|
-
total: number;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
const SearchComponent = () => {
|
|
1185
|
-
const {
|
|
1186
|
-
loading,
|
|
1187
|
-
result,
|
|
1188
|
-
error,
|
|
1189
|
-
run,
|
|
1190
|
-
cancel,
|
|
1191
|
-
isPending,
|
|
1192
|
-
setQuery,
|
|
1193
|
-
getQuery,
|
|
1194
|
-
} = useDebouncedFetcherQuery<SearchQuery, SearchResult>({
|
|
1195
|
-
url: '/api/search',
|
|
1196
|
-
initialQuery: { keyword: '', limit: 10 },
|
|
1197
|
-
debounce: { delay: 300 }, // 防抖 300ms
|
|
1198
|
-
autoExecute: false, // 挂载时不执行
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
const handleSearch = (keyword: string) => {
|
|
1202
|
-
setQuery({ keyword, limit: 10 }); // 如果 autoExecute 为 true,这将触发防抖执行
|
|
1203
|
-
};
|
|
1204
|
-
|
|
1205
|
-
const handleManualSearch = () => {
|
|
1206
|
-
run(); // 使用当前查询手动防抖执行
|
|
1207
|
-
};
|
|
2741
|
+
包含以下内容的对象:
|
|
1208
2742
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
2743
|
+
- `loading`: 布尔值,表示获取当前是否正在执行
|
|
2744
|
+
- `result`: 获取的解析值
|
|
2745
|
+
- `error`: 执行期间发生的任何错误
|
|
2746
|
+
- `status`: 当前执行状态
|
|
2747
|
+
- `exchange`: 表示正在进行的获取操作的 FetchExchange 对象
|
|
2748
|
+
- `run`: 使用请求参数执行防抖获取的函数
|
|
2749
|
+
- `cancel`: 取消任何待处理防抖执行的函数
|
|
2750
|
+
- `isPending`: 布尔值,表示防抖调用是否待处理
|
|
1212
2751
|
|
|
1213
|
-
|
|
1214
|
-
if (error) return <div>错误: {error.message}</div>;
|
|
2752
|
+
#### useDebouncedFetcherQuery
|
|
1215
2753
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
onChange={(e) => handleSearch(e.target.value)}
|
|
1221
|
-
placeholder="搜索..."
|
|
1222
|
-
/>
|
|
1223
|
-
<button onClick={handleManualSearch} disabled={isPending()}>
|
|
1224
|
-
{isPending() ? '搜索中...' : '搜索'}
|
|
1225
|
-
</button>
|
|
1226
|
-
<button onClick={handleCancel}>取消</button>
|
|
1227
|
-
{result && (
|
|
1228
|
-
<div>
|
|
1229
|
-
找到 {result.total} 项:
|
|
1230
|
-
{result.items.map(item => (
|
|
1231
|
-
<div key={item.id}>{item.title}</div>
|
|
1232
|
-
))}
|
|
1233
|
-
</div>
|
|
1234
|
-
)}
|
|
1235
|
-
</div>
|
|
1236
|
-
);
|
|
1237
|
-
};
|
|
2754
|
+
```typescript
|
|
2755
|
+
function useDebouncedFetcherQuery<Q, R, E = FetcherError>(
|
|
2756
|
+
options: UseDebouncedFetcherQueryOptions<Q, R, E>,
|
|
2757
|
+
): UseDebouncedFetcherQueryReturn<Q, R, E>;
|
|
1238
2758
|
```
|
|
1239
2759
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
- **查询状态管理**: 使用 `setQuery` 和 `getQuery` 自动查询参数处理
|
|
1243
|
-
- **防抖执行**: 在快速用户输入期间防止过多 API 调用
|
|
1244
|
-
- **自动执行**: 可选的在查询参数更改时自动执行
|
|
1245
|
-
- **手动控制**: `run()` 用于手动执行,`cancel()` 用于取消
|
|
1246
|
-
- **待处理状态**: `isPending()` 检查防抖调用是否排队
|
|
2760
|
+
将基于查询的 HTTP 获取与防抖相结合,非常适合搜索输入和动态查询场景,您希望根据查询参数防抖 API 调用。
|
|
1247
2761
|
|
|
1248
2762
|
**类型参数:**
|
|
1249
2763
|
|
|
@@ -1278,93 +2792,13 @@ const SearchComponent = () => {
|
|
|
1278
2792
|
|
|
1279
2793
|
#### useDebouncedQuery
|
|
1280
2794
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
interface SearchQuery {
|
|
1287
|
-
keyword: string;
|
|
1288
|
-
limit: number;
|
|
1289
|
-
filters?: { category?: string };
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
interface SearchResult {
|
|
1293
|
-
items: Array<{ id: string; title: string }>;
|
|
1294
|
-
total: number;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
const SearchComponent = () => {
|
|
1298
|
-
const {
|
|
1299
|
-
loading,
|
|
1300
|
-
result,
|
|
1301
|
-
error,
|
|
1302
|
-
run,
|
|
1303
|
-
cancel,
|
|
1304
|
-
isPending,
|
|
1305
|
-
setQuery,
|
|
1306
|
-
getQuery,
|
|
1307
|
-
} = useDebouncedQuery<SearchQuery, SearchResult>({
|
|
1308
|
-
initialQuery: { keyword: '', limit: 10 },
|
|
1309
|
-
execute: async (query) => {
|
|
1310
|
-
const response = await fetch('/api/search', {
|
|
1311
|
-
method: 'POST',
|
|
1312
|
-
body: JSON.stringify(query),
|
|
1313
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1314
|
-
});
|
|
1315
|
-
return response.json();
|
|
1316
|
-
},
|
|
1317
|
-
debounce: { delay: 300 }, // 防抖 300ms
|
|
1318
|
-
autoExecute: false, // 挂载时不执行
|
|
1319
|
-
});
|
|
1320
|
-
|
|
1321
|
-
const handleSearch = (keyword: string) => {
|
|
1322
|
-
setQuery({ keyword, limit: 10 }); // 如果 autoExecute 为 true,这将触发防抖执行
|
|
1323
|
-
};
|
|
1324
|
-
|
|
1325
|
-
const handleManualSearch = () => {
|
|
1326
|
-
run(); // 使用当前查询手动防抖执行
|
|
1327
|
-
};
|
|
1328
|
-
|
|
1329
|
-
const handleCancel = () => {
|
|
1330
|
-
cancel(); // 取消任何待处理的防抖执行
|
|
1331
|
-
};
|
|
1332
|
-
|
|
1333
|
-
if (loading) return <div>搜索中...</div>;
|
|
1334
|
-
if (error) return <div>错误: {error.message}</div>;
|
|
1335
|
-
|
|
1336
|
-
return (
|
|
1337
|
-
<div>
|
|
1338
|
-
<input
|
|
1339
|
-
type="text"
|
|
1340
|
-
onChange={(e) => handleSearch(e.target.value)}
|
|
1341
|
-
placeholder="搜索..."
|
|
1342
|
-
/>
|
|
1343
|
-
<button onClick={handleManualSearch} disabled={isPending()}>
|
|
1344
|
-
{isPending() ? '搜索中...' : '搜索'}
|
|
1345
|
-
</button>
|
|
1346
|
-
<button onClick={handleCancel}>取消</button>
|
|
1347
|
-
{result && (
|
|
1348
|
-
<div>
|
|
1349
|
-
找到 {result.total} 项:
|
|
1350
|
-
{result.items.map(item => (
|
|
1351
|
-
<div key={item.id}>{item.title}</div>
|
|
1352
|
-
))}
|
|
1353
|
-
</div>
|
|
1354
|
-
)}
|
|
1355
|
-
</div>
|
|
1356
|
-
);
|
|
1357
|
-
};
|
|
2795
|
+
```typescript
|
|
2796
|
+
function useDebouncedQuery<Q, R, E = FetcherError>(
|
|
2797
|
+
options: UseDebouncedQueryOptions<Q, R, E>,
|
|
2798
|
+
): UseDebouncedQueryReturn<Q, R, E>;
|
|
1358
2799
|
```
|
|
1359
2800
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
- **查询状态管理**: 使用 `setQuery` 和 `getQuery` 自动查询参数处理
|
|
1363
|
-
- **防抖执行**: 在快速查询更改期间防止过多操作
|
|
1364
|
-
- **自动执行**: 可选的在查询参数更改时自动执行
|
|
1365
|
-
- **手动控制**: `run()` 用于手动执行,`cancel()` 用于取消
|
|
1366
|
-
- **待处理状态**: `isPending()` 检查防抖调用是否排队
|
|
1367
|
-
- **自定义执行**: 灵活的 execute 函数用于任何查询操作
|
|
2801
|
+
将通用查询执行与防抖相结合,非常适合自定义查询操作,您希望根据查询参数防抖执行。
|
|
1368
2802
|
|
|
1369
2803
|
**类型参数:**
|
|
1370
2804
|
|
|
@@ -1499,9 +2933,7 @@ function usePromiseState<R = unknown, E = unknown>(
|
|
|
1499
2933
|
- `setError`: 设置状态为 ERROR 并提供错误
|
|
1500
2934
|
- `setIdle`: 设置状态为 IDLE
|
|
1501
2935
|
|
|
1502
|
-
###
|
|
1503
|
-
|
|
1504
|
-
#### useRequestId
|
|
2936
|
+
### useRequestId
|
|
1505
2937
|
|
|
1506
2938
|
```typescript
|
|
1507
2939
|
function useRequestId(): UseRequestIdReturn;
|
|
@@ -1572,68 +3004,170 @@ React hook,用于使用 Map-like 接口管理多个 refs,允许通过键动
|
|
|
1572
3004
|
- `RefKey = string | number | symbol`
|
|
1573
3005
|
- `UseRefsReturn<T> extends Iterable<[RefKey, T]>`
|
|
1574
3006
|
|
|
1575
|
-
###
|
|
3007
|
+
### useQuery
|
|
1576
3008
|
|
|
1577
|
-
|
|
3009
|
+
```typescript
|
|
3010
|
+
function useQuery<Q, R, E = FetcherError>(
|
|
3011
|
+
options: UseQueryOptions<Q, R, E>,
|
|
3012
|
+
): UseQueryReturn<Q, R, E>;
|
|
3013
|
+
```
|
|
1578
3014
|
|
|
1579
|
-
|
|
3015
|
+
用于管理基于查询的异步操作的 React hook,具有自动状态管理和执行控制。
|
|
1580
3016
|
|
|
1581
|
-
|
|
1582
|
-
import { useEventSubscription } from '@ahoo-wang/fetcher-react';
|
|
1583
|
-
import { eventBus } from './eventBus';
|
|
3017
|
+
**类型参数:**
|
|
1584
3018
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
handler: {
|
|
1589
|
-
name: 'myEvent',
|
|
1590
|
-
handle: (event) => {
|
|
1591
|
-
console.log('收到事件:', event);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
});
|
|
3019
|
+
- `Q`: 查询参数的类型
|
|
3020
|
+
- `R`: 结果值的类型
|
|
3021
|
+
- `E`: 错误值的类型(默认为 FetcherError)
|
|
1595
3022
|
|
|
1596
|
-
|
|
1597
|
-
// 如需要,您也可以手动控制订阅
|
|
1598
|
-
const handleToggleSubscription = () => {
|
|
1599
|
-
if (someCondition) {
|
|
1600
|
-
subscribe();
|
|
1601
|
-
} else {
|
|
1602
|
-
unsubscribe();
|
|
1603
|
-
}
|
|
1604
|
-
};
|
|
3023
|
+
**参数:**
|
|
1605
3024
|
|
|
1606
|
-
|
|
1607
|
-
|
|
3025
|
+
- `options`: 查询的配置选项
|
|
3026
|
+
- `initialQuery`: 初始查询参数
|
|
3027
|
+
- `execute`: 使用给定参数和可选属性执行查询的函数
|
|
3028
|
+
- `autoExecute?`: 是否在挂载和查询更改时自动执行查询
|
|
3029
|
+
- 所有来自 `UseExecutePromiseOptions` 的选项
|
|
3030
|
+
|
|
3031
|
+
**返回值:**
|
|
3032
|
+
|
|
3033
|
+
包含查询状态和控制函数的对象:
|
|
3034
|
+
|
|
3035
|
+
- `loading`: 布尔值,指示查询当前是否正在执行
|
|
3036
|
+
- `result`: 查询的解析值
|
|
3037
|
+
- `error`: 执行期间发生的任何错误
|
|
3038
|
+
- `status`: 当前执行状态
|
|
3039
|
+
- `execute`: 使用当前参数执行查询的函数
|
|
3040
|
+
- `reset`: 重置 Promise 状态的函数
|
|
3041
|
+
- `abort`: 中止当前操作的函数
|
|
3042
|
+
- `getQuery`: 检索当前查询参数的函数
|
|
3043
|
+
- `setQuery`: 更新查询参数的函数
|
|
3044
|
+
|
|
3045
|
+
### useQueryState
|
|
3046
|
+
|
|
3047
|
+
```typescript
|
|
3048
|
+
function useQueryState<Q>(
|
|
3049
|
+
options: UseQueryStateOptions<Q>,
|
|
3050
|
+
): UseQueryStateReturn<Q>;
|
|
1608
3051
|
```
|
|
1609
3052
|
|
|
1610
|
-
|
|
3053
|
+
用于管理查询状态的 React hook,具有自动执行功能。
|
|
1611
3054
|
|
|
1612
|
-
|
|
1613
|
-
- **手动控制**: 提供 `subscribe` 和 `unsubscribe` 函数以进行额外控制
|
|
1614
|
-
- **类型安全**: 完全支持 TypeScript,具有泛型事件类型
|
|
1615
|
-
- **错误处理**: 对失败的订阅尝试记录警告
|
|
1616
|
-
- **事件总线集成**: 与 `@ahoo-wang/fetcher-eventbus` TypedEventBus 实例无缝配合
|
|
3055
|
+
**类型参数:**
|
|
1617
3056
|
|
|
1618
|
-
|
|
3057
|
+
- `Q`: 查询参数的类型
|
|
1619
3058
|
|
|
1620
|
-
|
|
3059
|
+
**参数:**
|
|
1621
3060
|
|
|
1622
|
-
|
|
3061
|
+
- `options`: hook 的配置选项
|
|
3062
|
+
- `initialQuery`: 要存储和管理的初始查询参数
|
|
3063
|
+
- `autoExecute?`: 是否在查询更改时或组件挂载时自动执行
|
|
3064
|
+
- `execute`: 使用当前查询参数执行的函数
|
|
3065
|
+
|
|
3066
|
+
**返回值:**
|
|
3067
|
+
|
|
3068
|
+
包含以下内容的对象:
|
|
3069
|
+
|
|
3070
|
+
- `getQuery`: 检索当前查询参数的函数
|
|
3071
|
+
- `setQuery`: 更新查询参数的函数。如果 autoExecute 为 true 会触发执行
|
|
3072
|
+
|
|
3073
|
+
### useMounted
|
|
3074
|
+
|
|
3075
|
+
```typescript
|
|
3076
|
+
function useMounted(): () => boolean;
|
|
3077
|
+
```
|
|
3078
|
+
|
|
3079
|
+
返回检查组件是否仍挂载的函数的 React hook。
|
|
3080
|
+
|
|
3081
|
+
**返回值:**
|
|
3082
|
+
|
|
3083
|
+
当组件仍挂载时返回 `true`,否则返回 `false` 的函数。
|
|
3084
|
+
|
|
3085
|
+
### useForceUpdate
|
|
3086
|
+
|
|
3087
|
+
```typescript
|
|
3088
|
+
function useForceUpdate(): () => void;
|
|
3089
|
+
```
|
|
3090
|
+
|
|
3091
|
+
返回强制组件重新渲染的函数的 React hook。
|
|
3092
|
+
|
|
3093
|
+
**返回值:**
|
|
3094
|
+
|
|
3095
|
+
调用时强制组件重新渲染的函数。
|
|
3096
|
+
|
|
3097
|
+
### useEventSubscription
|
|
3098
|
+
|
|
3099
|
+
```typescript
|
|
3100
|
+
function useEventSubscription<EVENT = unknown>(
|
|
3101
|
+
options: UseEventSubscriptionOptions<EVENT>,
|
|
3102
|
+
): UseEventSubscriptionReturn;
|
|
3103
|
+
```
|
|
3104
|
+
|
|
3105
|
+
为类型化事件总线提供 React 接口的 hook。自动管理订阅生命周期,同时提供手动控制功能。
|
|
3106
|
+
|
|
3107
|
+
**类型参数:**
|
|
3108
|
+
|
|
3109
|
+
- `EVENT`: 事件总线处理的事件类型(默认为 unknown)
|
|
3110
|
+
|
|
3111
|
+
**参数:**
|
|
3112
|
+
|
|
3113
|
+
- `options`: 订阅的配置选项
|
|
3114
|
+
- `bus`: 要订阅的 TypedEventBus 实例
|
|
3115
|
+
- `handler`: 具有名称和处理方法的事件处理函数
|
|
3116
|
+
|
|
3117
|
+
**返回值:**
|
|
3118
|
+
|
|
3119
|
+
包含以下内容的对象:
|
|
3120
|
+
|
|
3121
|
+
- `subscribe`: 手动订阅事件总线的函数(返回布尔值成功状态)
|
|
3122
|
+
- `unsubscribe`: 手动取消订阅事件总线的函数(返回布尔值成功状态)
|
|
3123
|
+
|
|
3124
|
+
**相关类型:**
|
|
3125
|
+
|
|
3126
|
+
- `UseEventSubscriptionOptions<EVENT>`: 具有 bus 和 handler 属性的配置接口
|
|
3127
|
+
- `UseEventSubscriptionReturn`: 具有 subscribe 和 unsubscribe 方法的返回接口
|
|
3128
|
+
|
|
3129
|
+
### useKeyStorage
|
|
3130
|
+
|
|
3131
|
+
```typescript
|
|
3132
|
+
// 不使用默认值 - 可能返回 null
|
|
1623
3133
|
function useKeyStorage<T>(
|
|
1624
3134
|
keyStorage: KeyStorage<T>,
|
|
1625
3135
|
): [T | null, (value: T) => void];
|
|
3136
|
+
|
|
3137
|
+
// 使用默认值 - 保证非空
|
|
3138
|
+
function useKeyStorage<T>(
|
|
3139
|
+
keyStorage: KeyStorage<T>,
|
|
3140
|
+
defaultValue: T,
|
|
3141
|
+
): [T, (value: T) => void];
|
|
1626
3142
|
```
|
|
1627
3143
|
|
|
1628
|
-
为 KeyStorage
|
|
3144
|
+
为 KeyStorage 实例提供响应式状态管理的 React hook。订阅存储更改并返回当前值以及设置器函数。可选择接受默认值以在存储为空时使用。
|
|
3145
|
+
|
|
3146
|
+
**类型参数:**
|
|
3147
|
+
|
|
3148
|
+
- `T`: 存储在键存储中的值的类型
|
|
1629
3149
|
|
|
1630
3150
|
**参数:**
|
|
1631
3151
|
|
|
1632
|
-
- `keyStorage`: 要订阅和管理的 KeyStorage
|
|
3152
|
+
- `keyStorage`: 要订阅和管理的 KeyStorage 实例。应该是稳定引用(useRef、memo 或模块级实例)
|
|
3153
|
+
- `defaultValue` _(可选)_: 当存储为空时使用的默认值。提供时,hook 保证返回的值永远不会为 null
|
|
1633
3154
|
|
|
1634
3155
|
**返回值:**
|
|
1635
3156
|
|
|
1636
|
-
-
|
|
3157
|
+
- **不使用默认值**: `[T | null, (value: T) => void]` - 元组,其中第一个元素在存储为空时可能为 null
|
|
3158
|
+
- **使用默认值**: `[T, (value: T) => void]` - 元组,其中第一个元素保证不为 null(存储的值或默认值)
|
|
3159
|
+
|
|
3160
|
+
**示例:**
|
|
3161
|
+
|
|
3162
|
+
```typescript
|
|
3163
|
+
// 不使用默认值
|
|
3164
|
+
const [value, setValue] = useKeyStorage(keyStorage);
|
|
3165
|
+
// value: string | null
|
|
3166
|
+
|
|
3167
|
+
// 使用默认值
|
|
3168
|
+
const [theme, setTheme] = useKeyStorage(themeStorage, 'light');
|
|
3169
|
+
// theme: string (永不为 null)
|
|
3170
|
+
```
|
|
1637
3171
|
|
|
1638
3172
|
### useImmerKeyStorage
|
|
1639
3173
|
|
|
@@ -1658,11 +3192,11 @@ function useImmerKeyStorage<T>(
|
|
|
1658
3192
|
|
|
1659
3193
|
**类型参数:**
|
|
1660
3194
|
|
|
1661
|
-
- `T`:
|
|
3195
|
+
- `T`: 存储在键存储中的值的类型
|
|
1662
3196
|
|
|
1663
3197
|
**参数:**
|
|
1664
3198
|
|
|
1665
|
-
- `keyStorage`: 要订阅和管理的 KeyStorage
|
|
3199
|
+
- `keyStorage`: 要订阅和管理的 KeyStorage 实例。应该是稳定引用(useRef、memo 或模块级实例)
|
|
1666
3200
|
- `defaultValue` _(可选)_: 当存储为空时使用的默认值。提供时,hook 保证返回的值永远不会为 null
|
|
1667
3201
|
|
|
1668
3202
|
**返回值:**
|
|
@@ -1721,7 +3255,7 @@ function useListQuery<R, FIELDS extends string = string, E = FetcherError>(
|
|
|
1721
3255
|
**参数:**
|
|
1722
3256
|
|
|
1723
3257
|
- `options`: 包含 initialQuery 和 list 函数的配置选项
|
|
1724
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3258
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1725
3259
|
|
|
1726
3260
|
**返回值:**
|
|
1727
3261
|
|
|
@@ -1746,7 +3280,7 @@ function usePagedQuery<R, FIELDS extends string = string, E = unknown>(
|
|
|
1746
3280
|
**参数:**
|
|
1747
3281
|
|
|
1748
3282
|
- `options`: 包含 initialQuery 和 query 函数的配置选项
|
|
1749
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3283
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1750
3284
|
|
|
1751
3285
|
**返回值:**
|
|
1752
3286
|
|
|
@@ -1771,7 +3305,7 @@ function useSingleQuery<R, FIELDS extends string = string, E = unknown>(
|
|
|
1771
3305
|
**参数:**
|
|
1772
3306
|
|
|
1773
3307
|
- `options`: 包含 initialQuery 和 query 函数的配置选项
|
|
1774
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3308
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1775
3309
|
|
|
1776
3310
|
**返回值:**
|
|
1777
3311
|
|
|
@@ -1795,7 +3329,7 @@ function useCountQuery<FIELDS extends string = string, E = FetcherError>(
|
|
|
1795
3329
|
**参数:**
|
|
1796
3330
|
|
|
1797
3331
|
- `options`: 包含 initialQuery 和 execute 函数的配置选项
|
|
1798
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3332
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1799
3333
|
|
|
1800
3334
|
**返回值:**
|
|
1801
3335
|
|
|
@@ -1821,7 +3355,7 @@ function useFetcherCountQuery<FIELDS extends string = string, E = FetcherError>(
|
|
|
1821
3355
|
- `options`: 计数查询的配置选项,包括条件、fetcher 实例和其他查询设置
|
|
1822
3356
|
- `url`: 从中获取计数的 URL
|
|
1823
3357
|
- `initialQuery`: 计数查询的初始条件
|
|
1824
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3358
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1825
3359
|
|
|
1826
3360
|
**返回值:**
|
|
1827
3361
|
|
|
@@ -1852,7 +3386,7 @@ function useFetcherPagedQuery<
|
|
|
1852
3386
|
- `options`: 分页查询的配置选项,包括分页查询参数、fetcher 实例和其他查询设置
|
|
1853
3387
|
- `url`: 从中获取分页数据的 URL
|
|
1854
3388
|
- `initialQuery`: 初始分页查询配置
|
|
1855
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3389
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1856
3390
|
|
|
1857
3391
|
**返回值:**
|
|
1858
3392
|
|
|
@@ -1883,7 +3417,7 @@ function useFetcherListQuery<
|
|
|
1883
3417
|
- `options`: 列表查询的配置选项,包括列表查询参数、fetcher 实例和其他查询设置
|
|
1884
3418
|
- `url`: 从中获取列表数据的 URL
|
|
1885
3419
|
- `initialQuery`: 初始列表查询配置
|
|
1886
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3420
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1887
3421
|
|
|
1888
3422
|
**返回值:**
|
|
1889
3423
|
|
|
@@ -1914,7 +3448,7 @@ function useFetcherListStreamQuery<
|
|
|
1914
3448
|
- `options`: 列表流查询的配置选项,包括列表查询参数、fetcher 实例和其他查询设置
|
|
1915
3449
|
- `url`: 从中获取流数据的 URL
|
|
1916
3450
|
- `initialQuery`: 初始列表查询配置
|
|
1917
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3451
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1918
3452
|
|
|
1919
3453
|
**返回值:**
|
|
1920
3454
|
|
|
@@ -1945,7 +3479,7 @@ function useFetcherSingleQuery<
|
|
|
1945
3479
|
- `options`: 单个查询的配置选项,包括单个查询参数、fetcher 实例和其他查询设置
|
|
1946
3480
|
- `url`: 从中获取单个项目的 URL
|
|
1947
3481
|
- `initialQuery`: 初始单个查询配置
|
|
1948
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3482
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1949
3483
|
|
|
1950
3484
|
**返回值:**
|
|
1951
3485
|
|
|
@@ -1974,7 +3508,7 @@ function useListStreamQuery<
|
|
|
1974
3508
|
**参数:**
|
|
1975
3509
|
|
|
1976
3510
|
- `options`: 包含 initialQuery 和 listStream 函数的配置选项
|
|
1977
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3511
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1978
3512
|
|
|
1979
3513
|
**返回值:**
|
|
1980
3514
|
|