@ahoo-wang/fetcher-react 3.5.8 → 3.6.1
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 +868 -609
- package/README.zh-CN.md +2079 -672
- 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 +3 -3
- package/dist/core/useQuery.d.ts.map +1 -0
- package/dist/{wow → core}/useQueryState.d.ts +2 -2
- package/dist/core/useQueryState.d.ts.map +1 -0
- package/dist/cosec/useSecurity.d.ts +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 +4 -4
- package/dist/fetcher/useFetcherQuery.d.ts.map +1 -0
- package/dist/index.es.js +573 -552
- 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 +7 -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 +2 -2
- 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,21 +27,27 @@
|
|
|
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)
|
|
@@ -49,20 +55,20 @@
|
|
|
49
55
|
- [useSecurity](#usesecurity-hook)
|
|
50
56
|
- [SecurityProvider](#securityprovider)
|
|
51
57
|
- [useSecurityContext](#usesecuritycontext-hook)
|
|
58
|
+
- [RouteGuard](#routeguard)
|
|
52
59
|
- [Wow 查询 Hooks](#wow-查询-hooks)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- [
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- [useFetcherListStreamQuery](#usefetcherliststreamquery-hook)
|
|
60
|
+
- [基础查询 Hooks](#基础查询-hooks)
|
|
61
|
+
- [useListQuery](#uselistquery-hook)
|
|
62
|
+
- [usePagedQuery](#usepagedquery-hook)
|
|
63
|
+
- [useSingleQuery](#usesinglequery-hook)
|
|
64
|
+
- [useCountQuery](#usecountquery-hook)
|
|
65
|
+
- [useListStreamQuery](#useliststreamquery-hook)
|
|
66
|
+
- [Fetcher 查询 Hooks](#fetcher-查询-hooks)
|
|
67
|
+
- [useFetcherListQuery](#usefetcherlistquery-hook)
|
|
68
|
+
- [useFetcherPagedQuery](#usefetcherpagedquery-hook)
|
|
69
|
+
- [useFetcherSingleQuery](#usefetchersinglequery-hook)
|
|
70
|
+
- [useFetcherCountQuery](#usefetchercountquery-hook)
|
|
71
|
+
- [useFetcherListStreamQuery](#usefetcherliststreamquery-hook)
|
|
66
72
|
- [最佳实践](#最佳实践)
|
|
67
73
|
- [API 参考](#api-参考)
|
|
68
74
|
- [许可证](#许可证)
|
|
@@ -105,286 +111,1241 @@ function App() {
|
|
|
105
111
|
|
|
106
112
|
### 核心 Hooks
|
|
107
113
|
|
|
108
|
-
####
|
|
114
|
+
#### useExecutePromise
|
|
109
115
|
|
|
110
|
-
`
|
|
116
|
+
`useExecutePromise` hook 用于管理异步操作,具有自动状态处理、竞态条件保护和 Promise 状态选项。它包含用于取消操作的自动 AbortController 支持。
|
|
111
117
|
|
|
112
118
|
```typescript jsx
|
|
113
|
-
import {
|
|
119
|
+
import { useExecutePromise } from '@ahoo-wang/fetcher-react';
|
|
114
120
|
|
|
115
121
|
const MyComponent = () => {
|
|
116
|
-
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
|
+
};
|
|
117
132
|
|
|
118
133
|
const handleFetch = () => {
|
|
119
|
-
execute(
|
|
120
|
-
};
|
|
121
|
-
```
|
|
134
|
+
execute(fetchData); // 使用 Promise 供应商
|
|
135
|
+
};
|
|
122
136
|
|
|
123
|
-
|
|
137
|
+
const handleDirectPromise = () => {
|
|
138
|
+
const promise = fetch('/api/data').then(res => res.text());
|
|
139
|
+
execute(promise); // 使用直接 Promise
|
|
140
|
+
};
|
|
124
141
|
|
|
125
|
-
|
|
142
|
+
const handleAbort = () => {
|
|
143
|
+
abort(); // 手动中止当前操作
|
|
144
|
+
};
|
|
126
145
|
|
|
127
|
-
|
|
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
|
+
```
|
|
128
159
|
|
|
129
|
-
|
|
130
|
-
- **深度对象支持**: 轻松处理嵌套对象和数组
|
|
131
|
-
- **类型安全**: 完整的 TypeScript 支持和编译时错误检查
|
|
132
|
-
- **性能优化**: 利用 Immer 的结构共享和最小化重渲染
|
|
133
|
-
- **自动同步**: 变更自动持久化到存储并跨组件同步
|
|
160
|
+
##### 中止控制器支持
|
|
134
161
|
|
|
135
|
-
|
|
162
|
+
该 hook 会为每个操作自动创建一个 AbortController,并提供管理取消的方法:
|
|
136
163
|
|
|
137
|
-
|
|
164
|
+
- **自动清理**: 组件卸载时操作会自动中止
|
|
165
|
+
- **手动中止**: 使用 `abort()` 方法中止正在进行的操作
|
|
166
|
+
- **onAbort 回调**: 配置在操作中止时触发的回调(手动或自动)
|
|
167
|
+
- **AbortController 访问**: AbortController 会传递给 Promise 供应商以进行高级取消处理
|
|
138
168
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- 处理深度嵌套的数据结构
|
|
169
|
+
#### usePromiseState
|
|
170
|
+
|
|
171
|
+
`usePromiseState` hook 提供 Promise 操作的状态管理,无执行逻辑。支持静态选项和动态选项供应商。
|
|
143
172
|
|
|
144
173
|
```typescript jsx
|
|
145
|
-
import {
|
|
146
|
-
import { useImmerKeyStorage } from '@ahoo-wang/fetcher-react';
|
|
174
|
+
import { usePromiseState, PromiseStatus } from '@ahoo-wang/fetcher-react';
|
|
147
175
|
|
|
148
176
|
const MyComponent = () => {
|
|
149
|
-
const
|
|
150
|
-
theme: string;
|
|
151
|
-
volume: number;
|
|
152
|
-
notifications: boolean;
|
|
153
|
-
shortcuts: { [key: string]: string };
|
|
154
|
-
}>({
|
|
155
|
-
key: 'user-prefs'
|
|
156
|
-
});
|
|
177
|
+
const { status, loading, result, error, setSuccess, setError, setIdle } = usePromiseState<string>();
|
|
157
178
|
|
|
158
|
-
|
|
159
|
-
const
|
|
179
|
+
const handleSuccess = () => setSuccess('数据已加载');
|
|
180
|
+
const handleError = () => setError(new Error('加载失败'));
|
|
160
181
|
|
|
161
182
|
return (
|
|
162
183
|
<div>
|
|
163
|
-
<
|
|
164
|
-
<button onClick={
|
|
165
|
-
|
|
166
|
-
</
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
</
|
|
170
|
-
<button onClick={clearPrefs}>
|
|
171
|
-
清除偏好设置
|
|
172
|
-
</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>}
|
|
173
191
|
</div>
|
|
174
192
|
);
|
|
175
193
|
};
|
|
176
194
|
```
|
|
177
195
|
|
|
178
|
-
|
|
196
|
+
##### 使用选项供应商的 usePromiseState
|
|
179
197
|
|
|
180
198
|
```typescript jsx
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
},
|
|
184
213
|
});
|
|
185
214
|
|
|
186
|
-
|
|
187
|
-
const [settings, updateSettings, resetSettings] = useImmerKeyStorage(
|
|
188
|
-
settingsStorage,
|
|
189
|
-
{ volume: 50, muted: false }
|
|
190
|
-
);
|
|
215
|
+
const { setSuccess, setError } = usePromiseState<string>(optionsSupplier);
|
|
191
216
|
|
|
192
217
|
return (
|
|
193
218
|
<div>
|
|
194
|
-
<
|
|
195
|
-
<button onClick={() =>
|
|
196
|
-
draft.volume = Math.min(100, draft.volume + 10);
|
|
197
|
-
draft.muted = false;
|
|
198
|
-
})}>
|
|
199
|
-
增加音量
|
|
200
|
-
</button>
|
|
201
|
-
<button onClick={() => updateSettings(draft => { draft.muted = !draft.muted; })}>
|
|
202
|
-
切换静音
|
|
203
|
-
</button>
|
|
204
|
-
<button onClick={resetSettings}>
|
|
205
|
-
重置为默认值
|
|
206
|
-
</button>
|
|
219
|
+
<button onClick={() => setSuccess('动态成功!')}>设置成功</button>
|
|
220
|
+
<button onClick={() => setError(new Error('动态错误!'))}>设置错误</button>
|
|
207
221
|
</div>
|
|
208
222
|
);
|
|
209
223
|
};
|
|
210
224
|
```
|
|
211
225
|
|
|
212
|
-
####
|
|
226
|
+
#### useRequestId
|
|
213
227
|
|
|
214
|
-
|
|
228
|
+
`useRequestId` hook 提供请求 ID 管理,用于防止异步操作中的竞态条件。
|
|
215
229
|
|
|
216
230
|
```typescript jsx
|
|
217
|
-
|
|
218
|
-
updatePrefs(draft => {
|
|
219
|
-
draft.theme = 'dark';
|
|
220
|
-
draft.notifications = true;
|
|
221
|
-
draft.volume = 75;
|
|
222
|
-
});
|
|
223
|
-
};
|
|
224
|
-
```
|
|
231
|
+
import { useRequestId } from '@ahoo-wang/fetcher-react';
|
|
225
232
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
```typescript jsx
|
|
229
|
-
const todoStorage = new KeyStorage<{
|
|
230
|
-
todos: Array<{ id: number; text: string; done: boolean }>;
|
|
231
|
-
}>({
|
|
232
|
-
key: 'todos',
|
|
233
|
-
});
|
|
233
|
+
const MyComponent = () => {
|
|
234
|
+
const { generate, isLatest, invalidate } = useRequestId();
|
|
234
235
|
|
|
235
|
-
const
|
|
236
|
+
const handleFetch = async () => {
|
|
237
|
+
const requestId = generate();
|
|
236
238
|
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
updateState(draft => {
|
|
240
|
-
draft.todos.push({
|
|
241
|
-
id: Date.now(),
|
|
242
|
-
text,
|
|
243
|
-
done: false,
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
};
|
|
239
|
+
try {
|
|
240
|
+
const result = await fetchData();
|
|
247
241
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
242
|
+
if (isLatest(requestId)) {
|
|
243
|
+
setData(result);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (isLatest(requestId)) {
|
|
247
|
+
setError(error);
|
|
248
|
+
}
|
|
254
249
|
}
|
|
255
|
-
}
|
|
256
|
-
};
|
|
250
|
+
};
|
|
257
251
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
252
|
+
return (
|
|
253
|
+
<div>
|
|
254
|
+
<button onClick={handleFetch}>获取数据</button>
|
|
255
|
+
<button onClick={invalidate}>取消进行中</button>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
263
258
|
};
|
|
264
259
|
```
|
|
265
260
|
|
|
266
|
-
|
|
261
|
+
#### useLatest
|
|
262
|
+
|
|
263
|
+
`useLatest` hook 返回包含最新值的 ref 对象,用于在异步回调中访问当前值。
|
|
267
264
|
|
|
268
265
|
```typescript jsx
|
|
269
|
-
|
|
270
|
-
ui: { theme: string; language: string };
|
|
271
|
-
features: { [key: string]: boolean };
|
|
272
|
-
}>({
|
|
273
|
-
key: 'app-config',
|
|
274
|
-
});
|
|
266
|
+
import { useLatest } from '@ahoo-wang/fetcher-react';
|
|
275
267
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
});
|
|
268
|
+
const MyComponent = () => {
|
|
269
|
+
const [count, setCount] = useState(0);
|
|
270
|
+
const latestCount = useLatest(count);
|
|
280
271
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
286
|
-
};
|
|
272
|
+
const handleAsync = async () => {
|
|
273
|
+
await someAsyncOperation();
|
|
274
|
+
console.log('最新计数:', latestCount.current); // 始终是最新的
|
|
275
|
+
};
|
|
287
276
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
277
|
+
return (
|
|
278
|
+
<div>
|
|
279
|
+
<p>计数: {count}</p>
|
|
280
|
+
<button onClick={() => setCount(c => c + 1)}>增加</button>
|
|
281
|
+
<button onClick={handleAsync}>异步日志</button>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
292
284
|
};
|
|
293
285
|
```
|
|
294
286
|
|
|
295
|
-
|
|
287
|
+
#### useRefs
|
|
288
|
+
|
|
289
|
+
`useRefs` hook 提供 Map-like 接口用于动态管理多个 React refs。它允许通过键注册、检索和管理 refs,并在组件卸载时自动清理。
|
|
296
290
|
|
|
297
291
|
```typescript jsx
|
|
298
|
-
|
|
299
|
-
updateSettings(draft => {
|
|
300
|
-
if (newVolume >= 0 && newVolume <= 100) {
|
|
301
|
-
draft.volume = newVolume;
|
|
302
|
-
draft.muted = false; // 音量改变时取消静音
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
};
|
|
306
|
-
```
|
|
292
|
+
import { useRefs } from '@ahoo-wang/fetcher-react';
|
|
307
293
|
|
|
308
|
-
|
|
294
|
+
const MyComponent = () => {
|
|
295
|
+
const refs = useRefs<HTMLDivElement>();
|
|
309
296
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
};
|
|
297
|
+
const handleFocus = (key: string) => {
|
|
298
|
+
const element = refs.get(key);
|
|
299
|
+
element?.focus();
|
|
300
|
+
};
|
|
315
301
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
);
|
|
319
310
|
};
|
|
320
311
|
```
|
|
321
312
|
|
|
322
|
-
|
|
313
|
+
主要特性:
|
|
323
314
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
} catch (error) {
|
|
329
|
-
console.error('更新偏好设置失败:', error);
|
|
330
|
-
// 适当处理错误
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
```
|
|
315
|
+
- **动态注册**: 使用字符串、数字或符号键注册 refs
|
|
316
|
+
- **Map-like API**: 完整的 Map 接口,包括 get、set、has、delete 等
|
|
317
|
+
- **自动清理**: 组件卸载时清除 refs
|
|
318
|
+
- **类型安全**: 完整的 TypeScript 支持 ref 类型
|
|
334
319
|
|
|
335
|
-
####
|
|
320
|
+
#### useQuery
|
|
336
321
|
|
|
337
|
-
|
|
322
|
+
`useQuery` hook 提供完整的查询基础异步操作管理解决方案,具有自动状态管理和执行控制。
|
|
338
323
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
- 在单个更新调用中组合多个相关变更
|
|
342
|
-
- 对保证非空状态使用默认值
|
|
343
|
-
- 在更新函数中适当处理错误
|
|
324
|
+
```typescript jsx
|
|
325
|
+
import { useQuery } from '@ahoo-wang/fetcher-react';
|
|
344
326
|
|
|
345
|
-
|
|
327
|
+
interface UserQuery {
|
|
328
|
+
id: string;
|
|
329
|
+
}
|
|
346
330
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
331
|
+
interface User {
|
|
332
|
+
id: string;
|
|
333
|
+
name: string;
|
|
334
|
+
}
|
|
351
335
|
|
|
352
|
-
|
|
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
|
+
});
|
|
353
345
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
- 如果处理非常大的对象,请分析更新性能
|
|
346
|
+
const handleUserChange = (userId: string) => {
|
|
347
|
+
setQuery({ id: userId }); // 如果 autoExecute 为 true 会自动执行
|
|
348
|
+
};
|
|
358
349
|
|
|
359
|
-
|
|
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
|
+
```
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
// 为更好的安全性定义严格类型
|
|
363
|
-
type UserPreferences = {
|
|
364
|
-
theme: 'light' | 'dark' | 'auto';
|
|
365
|
-
volume: number; // 0-100
|
|
366
|
-
notifications: boolean;
|
|
367
|
-
shortcuts: Record<string, string>;
|
|
368
|
-
};
|
|
361
|
+
#### useQueryState
|
|
369
362
|
|
|
370
|
-
|
|
371
|
-
key: 'user-prefs',
|
|
372
|
-
});
|
|
363
|
+
`useQueryState` hook 提供查询参数的状态管理,具有自动执行功能。
|
|
373
364
|
|
|
374
|
-
|
|
375
|
-
|
|
365
|
+
```typescript jsx
|
|
366
|
+
import { useQueryState } from '@ahoo-wang/fetcher-react';
|
|
376
367
|
|
|
377
|
-
|
|
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);
|
|
1171
|
+
|
|
1172
|
+
// 这将导致 TypeScript 错误:
|
|
378
1173
|
// updatePrefs(draft => { draft.theme = 'invalid'; });
|
|
379
1174
|
```
|
|
380
1175
|
|
|
381
|
-
|
|
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
|
|
382
1343
|
|
|
383
1344
|
Wow 查询 Hooks 提供高级数据查询功能,具有内置的状态管理,用于条件、投影、排序、分页和限制。这些 hooks 专为与 `@ahoo-wang/fetcher-wow` 包配合使用而设计,用于复杂的查询操作。
|
|
384
1345
|
|
|
385
|
-
|
|
1346
|
+
#### 基础查询 Hooks
|
|
386
1347
|
|
|
387
|
-
|
|
1348
|
+
##### useListQuery
|
|
388
1349
|
|
|
389
1350
|
`useListQuery` hook 管理列表查询,具有条件、投影、排序和限制的状态管理。
|
|
390
1351
|
|
|
@@ -395,7 +1356,7 @@ const MyComponent = () => {
|
|
|
395
1356
|
const { result, loading, error, execute, setCondition, setLimit } = useListQuery({
|
|
396
1357
|
initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
|
|
397
1358
|
execute: async (listQuery) => {
|
|
398
|
-
//
|
|
1359
|
+
// 您的列表获取逻辑
|
|
399
1360
|
return fetchListData(listQuery);
|
|
400
1361
|
},
|
|
401
1362
|
});
|
|
@@ -421,7 +1382,37 @@ const MyComponent = () => {
|
|
|
421
1382
|
};
|
|
422
1383
|
```
|
|
423
1384
|
|
|
424
|
-
|
|
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
|
|
425
1416
|
|
|
426
1417
|
`usePagedQuery` hook 管理分页查询,具有条件、投影、分页和排序的状态管理。
|
|
427
1418
|
|
|
@@ -437,7 +1428,7 @@ const MyComponent = () => {
|
|
|
437
1428
|
sort: []
|
|
438
1429
|
},
|
|
439
1430
|
execute: async (pagedQuery) => {
|
|
440
|
-
//
|
|
1431
|
+
// 您的分页获取逻辑
|
|
441
1432
|
return fetchPagedData(pagedQuery);
|
|
442
1433
|
},
|
|
443
1434
|
});
|
|
@@ -468,7 +1459,47 @@ const MyComponent = () => {
|
|
|
468
1459
|
};
|
|
469
1460
|
```
|
|
470
1461
|
|
|
471
|
-
|
|
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
|
|
472
1503
|
|
|
473
1504
|
`useSingleQuery` hook 管理单个查询,具有条件、投影和排序的状态管理。
|
|
474
1505
|
|
|
@@ -484,76 +1515,213 @@ const MyComponent = () => {
|
|
|
484
1515
|
},
|
|
485
1516
|
});
|
|
486
1517
|
|
|
487
|
-
const handleFetchUser = (userId: string) => {
|
|
488
|
-
setCondition({ id: userId });
|
|
489
|
-
execute();
|
|
490
|
-
};
|
|
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>
|
|
1613
|
+
</div>
|
|
1614
|
+
);
|
|
1615
|
+
};
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
##### useListStreamQuery
|
|
1619
|
+
|
|
1620
|
+
`useListStreamQuery` hook 管理列表流查询,返回服务器发送事件的 readable stream。
|
|
1621
|
+
|
|
1622
|
+
```typescript jsx
|
|
1623
|
+
import { useListStreamQuery } from '@ahoo-wang/fetcher-react';
|
|
1624
|
+
|
|
1625
|
+
const MyComponent = () => {
|
|
1626
|
+
const { result, loading, error, execute, setCondition } = useListStreamQuery({
|
|
1627
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 100 },
|
|
1628
|
+
execute: async (listQuery) => {
|
|
1629
|
+
// 您的流获取逻辑
|
|
1630
|
+
return fetchListStream(listQuery);
|
|
1631
|
+
},
|
|
1632
|
+
});
|
|
1633
|
+
|
|
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]);
|
|
491
1652
|
|
|
492
1653
|
if (loading) return <div>加载中...</div>;
|
|
493
1654
|
if (error) return <div>错误: {error.message}</div>;
|
|
494
1655
|
|
|
495
1656
|
return (
|
|
496
1657
|
<div>
|
|
497
|
-
<button onClick={
|
|
498
|
-
{result && <p>用户: {result.name}</p>}
|
|
1658
|
+
<button onClick={execute}>开始流</button>
|
|
499
1659
|
</div>
|
|
500
1660
|
);
|
|
501
1661
|
};
|
|
502
1662
|
```
|
|
503
1663
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
`useCountQuery` hook 管理计数查询,具有条件的状态管理。
|
|
1664
|
+
###### 自动执行示例
|
|
507
1665
|
|
|
508
1666
|
```typescript jsx
|
|
509
|
-
import {
|
|
1667
|
+
import { useListStreamQuery } from '@ahoo-wang/fetcher-react';
|
|
510
1668
|
|
|
511
1669
|
const MyComponent = () => {
|
|
512
|
-
const { result, loading, error, execute, setCondition } =
|
|
513
|
-
initialQuery: {},
|
|
514
|
-
execute: async (
|
|
515
|
-
|
|
516
|
-
return fetchCount(condition);
|
|
517
|
-
},
|
|
1670
|
+
const { result, loading, error, execute, setCondition } = useListStreamQuery({
|
|
1671
|
+
initialQuery: { condition: {}, projection: {}, sort: [], limit: 100 },
|
|
1672
|
+
execute: async (listQuery) => fetchListStream(listQuery),
|
|
1673
|
+
autoExecute: true, // 组件挂载时自动执行
|
|
518
1674
|
});
|
|
519
1675
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
+
// 流将在组件挂载时自动启动
|
|
524
1696
|
|
|
525
1697
|
if (loading) return <div>加载中...</div>;
|
|
526
1698
|
if (error) return <div>错误: {error.message}</div>;
|
|
527
1699
|
|
|
528
1700
|
return (
|
|
529
1701
|
<div>
|
|
530
|
-
|
|
531
|
-
<p>总数: {result}</p>
|
|
1702
|
+
{/* 流已自动启动 */}
|
|
532
1703
|
</div>
|
|
533
1704
|
);
|
|
534
1705
|
};
|
|
535
1706
|
```
|
|
536
1707
|
|
|
537
|
-
|
|
1708
|
+
#### Fetcher 查询 Hooks
|
|
538
1709
|
|
|
539
|
-
|
|
1710
|
+
##### useFetcherCountQuery
|
|
540
1711
|
|
|
541
1712
|
`useFetcherCountQuery` hook 是使用 Fetcher 库执行计数查询的专用 React hook。它专为需要检索匹配特定条件的记录数量的场景而设计,返回表示计数的数字。
|
|
542
1713
|
|
|
543
1714
|
```typescript jsx
|
|
544
1715
|
import { useFetcherCountQuery } from '@ahoo-wang/fetcher-react';
|
|
545
1716
|
import { all } from '@ahoo-wang/fetcher-wow';
|
|
546
|
-
|
|
547
1717
|
function UserCountComponent() {
|
|
548
1718
|
const { data: count, loading, error, execute } = useFetcherCountQuery({
|
|
549
1719
|
url: '/api/users/count',
|
|
550
1720
|
initialQuery: all(),
|
|
551
1721
|
autoExecute: true,
|
|
552
1722
|
});
|
|
553
|
-
|
|
554
1723
|
if (loading) return <div>加载中...</div>;
|
|
555
1724
|
if (error) return <div>错误: {error.message}</div>;
|
|
556
|
-
|
|
557
1725
|
return (
|
|
558
1726
|
<div>
|
|
559
1727
|
<div>活跃用户总数: {count}</div>
|
|
@@ -563,23 +1731,19 @@ function UserCountComponent() {
|
|
|
563
1731
|
}
|
|
564
1732
|
```
|
|
565
1733
|
|
|
566
|
-
|
|
1734
|
+
###### 自动执行示例
|
|
567
1735
|
|
|
568
1736
|
```typescript jsx
|
|
569
1737
|
import { useFetcherCountQuery } from '@ahoo-wang/fetcher-react';
|
|
570
|
-
|
|
571
1738
|
const MyComponent = () => {
|
|
572
1739
|
const { data: count, loading, error, execute } = useFetcherCountQuery({
|
|
573
1740
|
url: '/api/users/count',
|
|
574
1741
|
initialQuery: { status: 'active' },
|
|
575
1742
|
autoExecute: true, // 组件挂载时自动执行
|
|
576
1743
|
});
|
|
577
|
-
|
|
578
1744
|
// 查询将在组件挂载时自动执行
|
|
579
|
-
|
|
580
1745
|
if (loading) return <div>加载中...</div>;
|
|
581
1746
|
if (error) return <div>错误: {error.message}</div>;
|
|
582
|
-
|
|
583
1747
|
return (
|
|
584
1748
|
<div>
|
|
585
1749
|
<p>活跃用户总数: {count}</p>
|
|
@@ -588,7 +1752,7 @@ const MyComponent = () => {
|
|
|
588
1752
|
};
|
|
589
1753
|
```
|
|
590
1754
|
|
|
591
|
-
|
|
1755
|
+
##### useFetcherPagedQuery
|
|
592
1756
|
|
|
593
1757
|
`useFetcherPagedQuery` hook 是使用 Fetcher 库执行分页查询的专用 React hook。它专为需要检索匹配查询条件的分页数据的场景而设计,返回包含当前页面项目以及分页元数据的 PagedList。
|
|
594
1758
|
|
|
@@ -657,7 +1821,7 @@ function UserListComponent() {
|
|
|
657
1821
|
}
|
|
658
1822
|
```
|
|
659
1823
|
|
|
660
|
-
|
|
1824
|
+
###### 自动执行示例
|
|
661
1825
|
|
|
662
1826
|
```typescript jsx
|
|
663
1827
|
import { useFetcherPagedQuery } from '@ahoo-wang/fetcher-react';
|
|
@@ -693,7 +1857,7 @@ const MyComponent = () => {
|
|
|
693
1857
|
};
|
|
694
1858
|
```
|
|
695
1859
|
|
|
696
|
-
|
|
1860
|
+
##### useFetcherListQuery
|
|
697
1861
|
|
|
698
1862
|
`useFetcherListQuery` hook 是使用 Fetcher 库执行列表查询的专用 React hook。它专为获取项目列表而设计,支持通过 ListQuery 类型进行过滤、排序和分页,返回结果数组。
|
|
699
1863
|
|
|
@@ -754,7 +1918,7 @@ function UserListComponent() {
|
|
|
754
1918
|
}
|
|
755
1919
|
```
|
|
756
1920
|
|
|
757
|
-
|
|
1921
|
+
###### 自动执行示例
|
|
758
1922
|
|
|
759
1923
|
```typescript jsx
|
|
760
1924
|
import { useFetcherListQuery } from '@ahoo-wang/fetcher-react';
|
|
@@ -789,7 +1953,7 @@ const MyComponent = () => {
|
|
|
789
1953
|
};
|
|
790
1954
|
```
|
|
791
1955
|
|
|
792
|
-
|
|
1956
|
+
##### useFetcherListStreamQuery
|
|
793
1957
|
|
|
794
1958
|
`useFetcherListStreamQuery` hook 是使用 Fetcher 库通过服务器发送事件执行列表流查询的专用 React hook。它专为需要检索匹配列表查询条件的数据流场景而设计,返回 JSON 服务器发送事件的 ReadableStream,用于实时数据流式传输。
|
|
795
1959
|
|
|
@@ -852,7 +2016,7 @@ function UserStreamComponent() {
|
|
|
852
2016
|
}
|
|
853
2017
|
```
|
|
854
2018
|
|
|
855
|
-
|
|
2019
|
+
###### 自动执行示例
|
|
856
2020
|
|
|
857
2021
|
```typescript jsx
|
|
858
2022
|
import { useFetcherListStreamQuery } from '@ahoo-wang/fetcher-react';
|
|
@@ -908,7 +2072,7 @@ const MyComponent = () => {
|
|
|
908
2072
|
};
|
|
909
2073
|
```
|
|
910
2074
|
|
|
911
|
-
|
|
2075
|
+
##### useFetcherSingleQuery
|
|
912
2076
|
|
|
913
2077
|
`useFetcherSingleQuery` hook 是使用 Fetcher 库执行单个项目查询的专用 React hook。它专为获取单个项目而设计,支持通过 SingleQuery 类型进行过滤和排序,返回单个结果项目。
|
|
914
2078
|
|
|
@@ -952,7 +2116,7 @@ function UserProfileComponent({ userId }: { userId: string }) {
|
|
|
952
2116
|
}
|
|
953
2117
|
```
|
|
954
2118
|
|
|
955
|
-
|
|
2119
|
+
###### 自动执行示例
|
|
956
2120
|
|
|
957
2121
|
```typescript jsx
|
|
958
2122
|
import { useFetcherSingleQuery } from '@ahoo-wang/fetcher-react';
|
|
@@ -968,95 +2132,519 @@ const MyComponent = () => {
|
|
|
968
2132
|
autoExecute: true, // 组件挂载时自动执行
|
|
969
2133
|
});
|
|
970
2134
|
|
|
971
|
-
// 查询将在组件挂载时自动执行
|
|
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>
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
```
|
|
2281
|
+
|
|
2282
|
+
### Suspense 集成
|
|
2283
|
+
|
|
2284
|
+
与 React Suspense 一起使用以获得更好的加载状态:
|
|
2285
|
+
|
|
2286
|
+
```typescript jsx
|
|
2287
|
+
import { Suspense, useState } from 'react';
|
|
2288
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
2289
|
+
|
|
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
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
function DataComponent({ resource }: { resource: any }) {
|
|
2321
|
+
const data = resource.read(); // 如果待处理将抛出
|
|
2322
|
+
return <div>{JSON.stringify(data)}</div>;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
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
|
+
};
|
|
2333
|
+
|
|
2334
|
+
return (
|
|
2335
|
+
<div>
|
|
2336
|
+
<button onClick={handleFetch}>获取数据</button>
|
|
2337
|
+
<Suspense fallback={<div>加载中...</div>}>
|
|
2338
|
+
{resource && <DataComponent resource={resource} />}
|
|
2339
|
+
</Suspense>
|
|
2340
|
+
</div>
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
```
|
|
2344
|
+
|
|
2345
|
+
### 性能优化模式
|
|
2346
|
+
|
|
2347
|
+
优化性能的高级模式:
|
|
2348
|
+
|
|
2349
|
+
```typescript jsx
|
|
2350
|
+
import { useMemo, useCallback, useRef } from 'react';
|
|
2351
|
+
import { useListQuery } from '@ahoo-wang/fetcher-react';
|
|
2352
|
+
|
|
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
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
// 使用 ref 跟踪最新过滤器而不引起重新渲染
|
|
2372
|
+
const filtersRef = useRef(filters);
|
|
2373
|
+
|
|
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
|
+
);
|
|
2385
|
+
|
|
2386
|
+
return (
|
|
2387
|
+
<div>
|
|
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
|
+
)}
|
|
2403
|
+
</div>
|
|
2404
|
+
);
|
|
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
|
+
}
|
|
2418
|
+
```
|
|
2419
|
+
|
|
2420
|
+
### 真实世界集成示例
|
|
2421
|
+
|
|
2422
|
+
显示与流行库集成的完整示例:
|
|
2423
|
+
|
|
2424
|
+
#### 与 React Query (TanStack Query) 集成
|
|
2425
|
+
|
|
2426
|
+
```typescript jsx
|
|
2427
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2428
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
2429
|
+
|
|
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
|
+
}
|
|
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);
|
|
2494
|
+
|
|
2495
|
+
useEffect(() => {
|
|
2496
|
+
dispatch(fetchUserData(userId));
|
|
2497
|
+
}, [userId, dispatch]);
|
|
972
2498
|
|
|
973
2499
|
if (loading) return <div>加载中...</div>;
|
|
974
|
-
if (error) return <div>错误: {error
|
|
975
|
-
if (!product) return <div>未找到产品</div>;
|
|
2500
|
+
if (error) return <div>错误: {error}</div>;
|
|
976
2501
|
|
|
977
|
-
return
|
|
978
|
-
|
|
979
|
-
<h2>特色产品</h2>
|
|
980
|
-
<div>{product.name}</div>
|
|
981
|
-
<div>{product.description}</div>
|
|
982
|
-
</div>
|
|
983
|
-
);
|
|
984
|
-
};
|
|
2502
|
+
return <div>{data?.name}</div>;
|
|
2503
|
+
}
|
|
985
2504
|
```
|
|
986
2505
|
|
|
987
|
-
|
|
2506
|
+
#### 与 Zustand 集成
|
|
988
2507
|
|
|
989
|
-
|
|
2508
|
+
```typescript jsx
|
|
2509
|
+
import { create } from 'zustand';
|
|
2510
|
+
import { useFetcher } from '@ahoo-wang/fetcher-react';
|
|
990
2511
|
|
|
991
|
-
|
|
2512
|
+
interface UserStore {
|
|
2513
|
+
user: any;
|
|
2514
|
+
loading: boolean;
|
|
2515
|
+
error: string | null;
|
|
2516
|
+
fetchUser: (userId: string) => Promise<void>;
|
|
2517
|
+
}
|
|
992
2518
|
|
|
993
|
-
|
|
994
|
-
|
|
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 });
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}));
|
|
995
2537
|
|
|
996
|
-
|
|
997
|
-
const {
|
|
998
|
-
initialQuery: { condition: {}, projection: {}, sort: [], limit: 100 },
|
|
999
|
-
execute: async (listQuery) => {
|
|
1000
|
-
// 您的流获取逻辑
|
|
1001
|
-
return fetchListStream(listQuery);
|
|
1002
|
-
},
|
|
1003
|
-
});
|
|
2538
|
+
function UserComponent({ userId }: { userId: string }) {
|
|
2539
|
+
const { user, loading, error, fetchUser } = useUserStore();
|
|
1004
2540
|
|
|
1005
2541
|
useEffect(() => {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const readStream = async () => {
|
|
1009
|
-
try {
|
|
1010
|
-
while (true) {
|
|
1011
|
-
const { done, value } = await reader.read();
|
|
1012
|
-
if (done) break;
|
|
1013
|
-
console.log('接收到:', value);
|
|
1014
|
-
// 处理流事件
|
|
1015
|
-
}
|
|
1016
|
-
} catch (error) {
|
|
1017
|
-
console.error('流错误:', error);
|
|
1018
|
-
}
|
|
1019
|
-
};
|
|
1020
|
-
readStream();
|
|
1021
|
-
}
|
|
1022
|
-
}, [result]);
|
|
2542
|
+
fetchUser(userId);
|
|
2543
|
+
}, [userId, fetchUser]);
|
|
1023
2544
|
|
|
1024
2545
|
if (loading) return <div>加载中...</div>;
|
|
1025
|
-
if (error) return <div>错误: {error
|
|
2546
|
+
if (error) return <div>错误: {error}</div>;
|
|
1026
2547
|
|
|
1027
|
-
return
|
|
1028
|
-
|
|
1029
|
-
<button onClick={execute}>开始流</button>
|
|
1030
|
-
</div>
|
|
1031
|
-
);
|
|
1032
|
-
};
|
|
2548
|
+
return <div>{user?.name}</div>;
|
|
2549
|
+
}
|
|
1033
2550
|
```
|
|
1034
2551
|
|
|
1035
|
-
|
|
2552
|
+
### 测试模式
|
|
1036
2553
|
|
|
1037
|
-
|
|
2554
|
+
Hooks 的综合测试示例:
|
|
1038
2555
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
2556
|
+
```typescript jsx
|
|
2557
|
+
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
2558
|
+
import { useFetcher, useListQuery } from '@ahoo-wang/fetcher-react';
|
|
1042
2559
|
|
|
1043
|
-
|
|
2560
|
+
// 模拟 fetcher
|
|
2561
|
+
jest.mock('@ahoo-wang/fetcher', () => ({
|
|
2562
|
+
Fetcher: jest.fn().mockImplementation(() => ({
|
|
2563
|
+
request: jest.fn(),
|
|
2564
|
+
})),
|
|
2565
|
+
}));
|
|
1044
2566
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
2567
|
+
describe('useFetcher', () => {
|
|
2568
|
+
it('应处理成功的获取', async () => {
|
|
2569
|
+
const mockData = { id: 1, name: 'Test' };
|
|
2570
|
+
const mockFetcher = { request: jest.fn().mockResolvedValue(mockData) };
|
|
1048
2571
|
|
|
1049
|
-
|
|
2572
|
+
const { result } = renderHook(() => useFetcher({ fetcher: mockFetcher }));
|
|
1050
2573
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
2574
|
+
act(() => {
|
|
2575
|
+
result.current.execute({ url: '/api/test', method: 'GET' });
|
|
2576
|
+
});
|
|
1054
2577
|
|
|
1055
|
-
|
|
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
|
+
});
|
|
1056
2584
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
+
```
|
|
1060
2648
|
|
|
1061
2649
|
## API 参考
|
|
1062
2650
|
|
|
@@ -1071,7 +2659,7 @@ function useDebouncedCallback<T extends (...args: any[]) => any>(
|
|
|
1071
2659
|
): UseDebouncedCallbackReturn<T>;
|
|
1072
2660
|
```
|
|
1073
2661
|
|
|
1074
|
-
|
|
2662
|
+
为回调函数提供防抖版本的 React hook,支持前缘/后缘执行选项。
|
|
1075
2663
|
|
|
1076
2664
|
**类型参数:**
|
|
1077
2665
|
|
|
@@ -1167,208 +2755,50 @@ function useDebouncedFetcher<R, E = FetcherError>(
|
|
|
1167
2755
|
function useDebouncedFetcherQuery<Q, R, E = FetcherError>(
|
|
1168
2756
|
options: UseDebouncedFetcherQueryOptions<Q, R, E>,
|
|
1169
2757
|
): UseDebouncedFetcherQueryReturn<Q, R, E>;
|
|
1170
|
-
```
|
|
1171
|
-
|
|
1172
|
-
将基于查询的 HTTP 获取与防抖相结合,非常适合搜索输入和动态查询场景,您希望根据查询参数防抖 API 调用。
|
|
1173
|
-
|
|
1174
|
-
```typescript jsx
|
|
1175
|
-
import { useDebouncedFetcherQuery } from '@ahoo-wang/fetcher-react';
|
|
1176
|
-
|
|
1177
|
-
interface SearchQuery {
|
|
1178
|
-
keyword: string;
|
|
1179
|
-
limit: number;
|
|
1180
|
-
filters?: { category?: string };
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
interface SearchResult {
|
|
1184
|
-
items: Array<{ id: string; title: string }>;
|
|
1185
|
-
total: number;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
const SearchComponent = () => {
|
|
1189
|
-
const {
|
|
1190
|
-
loading,
|
|
1191
|
-
result,
|
|
1192
|
-
error,
|
|
1193
|
-
run,
|
|
1194
|
-
cancel,
|
|
1195
|
-
isPending,
|
|
1196
|
-
setQuery,
|
|
1197
|
-
getQuery,
|
|
1198
|
-
} = useDebouncedFetcherQuery<SearchQuery, SearchResult>({
|
|
1199
|
-
url: '/api/search',
|
|
1200
|
-
initialQuery: { keyword: '', limit: 10 },
|
|
1201
|
-
debounce: { delay: 300 }, // 防抖 300ms
|
|
1202
|
-
autoExecute: false, // 挂载时不执行
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
const handleSearch = (keyword: string) => {
|
|
1206
|
-
setQuery({ keyword, limit: 10 }); // 如果 autoExecute 为 true,这将触发防抖执行
|
|
1207
|
-
};
|
|
1208
|
-
|
|
1209
|
-
const handleManualSearch = () => {
|
|
1210
|
-
run(); // 使用当前查询手动防抖执行
|
|
1211
|
-
};
|
|
1212
|
-
|
|
1213
|
-
const handleCancel = () => {
|
|
1214
|
-
cancel(); // 取消任何待处理的防抖执行
|
|
1215
|
-
};
|
|
1216
|
-
|
|
1217
|
-
if (loading) return <div>搜索中...</div>;
|
|
1218
|
-
if (error) return <div>错误: {error.message}</div>;
|
|
1219
|
-
|
|
1220
|
-
return (
|
|
1221
|
-
<div>
|
|
1222
|
-
<input
|
|
1223
|
-
type="text"
|
|
1224
|
-
onChange={(e) => handleSearch(e.target.value)}
|
|
1225
|
-
placeholder="搜索..."
|
|
1226
|
-
/>
|
|
1227
|
-
<button onClick={handleManualSearch} disabled={isPending()}>
|
|
1228
|
-
{isPending() ? '搜索中...' : '搜索'}
|
|
1229
|
-
</button>
|
|
1230
|
-
<button onClick={handleCancel}>取消</button>
|
|
1231
|
-
{result && (
|
|
1232
|
-
<div>
|
|
1233
|
-
找到 {result.total} 项:
|
|
1234
|
-
{result.items.map(item => (
|
|
1235
|
-
<div key={item.id}>{item.title}</div>
|
|
1236
|
-
))}
|
|
1237
|
-
</div>
|
|
1238
|
-
)}
|
|
1239
|
-
</div>
|
|
1240
|
-
);
|
|
1241
|
-
};
|
|
1242
|
-
```
|
|
1243
|
-
|
|
1244
|
-
**主要特性:**
|
|
1245
|
-
|
|
1246
|
-
- **查询状态管理**: 使用 `setQuery` 和 `getQuery` 自动查询参数处理
|
|
1247
|
-
- **防抖执行**: 在快速用户输入期间防止过多 API 调用
|
|
1248
|
-
- **自动执行**: 可选的在查询参数更改时自动执行
|
|
1249
|
-
- **手动控制**: `run()` 用于手动执行,`cancel()` 用于取消
|
|
1250
|
-
- **待处理状态**: `isPending()` 检查防抖调用是否排队
|
|
1251
|
-
|
|
1252
|
-
**类型参数:**
|
|
1253
|
-
|
|
1254
|
-
- `Q`: 查询参数的类型
|
|
1255
|
-
- `R`: 获取结果的类型
|
|
1256
|
-
- `E`: 错误的类型(默认为 FetcherError)
|
|
1257
|
-
|
|
1258
|
-
**参数:**
|
|
1259
|
-
|
|
1260
|
-
- `options`: 扩展 `UseFetcherQueryOptions` 和 `DebounceCapable` 的配置对象
|
|
1261
|
-
- `url`: API 端点 URL(必需)
|
|
1262
|
-
- `initialQuery`: 初始查询参数(必需)
|
|
1263
|
-
- `autoExecute?`: 查询参数更改时是否自动执行
|
|
1264
|
-
- `debounce`: 防抖配置(delay、leading、trailing)
|
|
1265
|
-
- HTTP 请求选项(method、headers、timeout 等)
|
|
1266
|
-
|
|
1267
|
-
**返回:**
|
|
1268
|
-
|
|
1269
|
-
包含以下内容的对象:
|
|
1270
|
-
|
|
1271
|
-
- `loading`: 布尔值,表示获取当前是否正在执行
|
|
1272
|
-
- `result`: 获取的解析值
|
|
1273
|
-
- `error`: 执行期间发生的任何错误
|
|
1274
|
-
- `status`: 当前执行状态
|
|
1275
|
-
- `reset`: 重置获取器状态的函数
|
|
1276
|
-
- `abort`: 中止当前操作的函数
|
|
1277
|
-
- `getQuery`: 获取当前查询参数的函数
|
|
1278
|
-
- `setQuery`: 更新查询参数的函数
|
|
1279
|
-
- `run`: 使用当前查询执行防抖获取的函数
|
|
1280
|
-
- `cancel`: 取消任何待处理防抖执行的函数
|
|
1281
|
-
- `isPending`: 返回布尔值表示防抖执行当前是否待处理的函数
|
|
1282
|
-
|
|
1283
|
-
#### useDebouncedQuery
|
|
1284
|
-
|
|
1285
|
-
将通用查询执行与防抖相结合,非常适合自定义查询操作,您希望根据查询参数防抖执行。
|
|
1286
|
-
|
|
1287
|
-
```typescript jsx
|
|
1288
|
-
import { useDebouncedQuery } from '@ahoo-wang/fetcher-react';
|
|
1289
|
-
|
|
1290
|
-
interface SearchQuery {
|
|
1291
|
-
keyword: string;
|
|
1292
|
-
limit: number;
|
|
1293
|
-
filters?: { category?: string };
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
interface SearchResult {
|
|
1297
|
-
items: Array<{ id: string; title: string }>;
|
|
1298
|
-
total: number;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
const SearchComponent = () => {
|
|
1302
|
-
const {
|
|
1303
|
-
loading,
|
|
1304
|
-
result,
|
|
1305
|
-
error,
|
|
1306
|
-
run,
|
|
1307
|
-
cancel,
|
|
1308
|
-
isPending,
|
|
1309
|
-
setQuery,
|
|
1310
|
-
getQuery,
|
|
1311
|
-
} = useDebouncedQuery<SearchQuery, SearchResult>({
|
|
1312
|
-
initialQuery: { keyword: '', limit: 10 },
|
|
1313
|
-
execute: async (query) => {
|
|
1314
|
-
const response = await fetch('/api/search', {
|
|
1315
|
-
method: 'POST',
|
|
1316
|
-
body: JSON.stringify(query),
|
|
1317
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1318
|
-
});
|
|
1319
|
-
return response.json();
|
|
1320
|
-
},
|
|
1321
|
-
debounce: { delay: 300 }, // 防抖 300ms
|
|
1322
|
-
autoExecute: false, // 挂载时不执行
|
|
1323
|
-
});
|
|
2758
|
+
```
|
|
1324
2759
|
|
|
1325
|
-
|
|
1326
|
-
setQuery({ keyword, limit: 10 }); // 如果 autoExecute 为 true,这将触发防抖执行
|
|
1327
|
-
};
|
|
2760
|
+
将基于查询的 HTTP 获取与防抖相结合,非常适合搜索输入和动态查询场景,您希望根据查询参数防抖 API 调用。
|
|
1328
2761
|
|
|
1329
|
-
|
|
1330
|
-
run(); // 使用当前查询手动防抖执行
|
|
1331
|
-
};
|
|
2762
|
+
**类型参数:**
|
|
1332
2763
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
2764
|
+
- `Q`: 查询参数的类型
|
|
2765
|
+
- `R`: 获取结果的类型
|
|
2766
|
+
- `E`: 错误的类型(默认为 FetcherError)
|
|
1336
2767
|
|
|
1337
|
-
|
|
1338
|
-
if (error) return <div>错误: {error.message}</div>;
|
|
2768
|
+
**参数:**
|
|
1339
2769
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
/>
|
|
1347
|
-
<button onClick={handleManualSearch} disabled={isPending()}>
|
|
1348
|
-
{isPending() ? '搜索中...' : '搜索'}
|
|
1349
|
-
</button>
|
|
1350
|
-
<button onClick={handleCancel}>取消</button>
|
|
1351
|
-
{result && (
|
|
1352
|
-
<div>
|
|
1353
|
-
找到 {result.total} 项:
|
|
1354
|
-
{result.items.map(item => (
|
|
1355
|
-
<div key={item.id}>{item.title}</div>
|
|
1356
|
-
))}
|
|
1357
|
-
</div>
|
|
1358
|
-
)}
|
|
1359
|
-
</div>
|
|
1360
|
-
);
|
|
1361
|
-
};
|
|
1362
|
-
```
|
|
2770
|
+
- `options`: 扩展 `UseFetcherQueryOptions` 和 `DebounceCapable` 的配置对象
|
|
2771
|
+
- `url`: API 端点 URL(必需)
|
|
2772
|
+
- `initialQuery`: 初始查询参数(必需)
|
|
2773
|
+
- `autoExecute?`: 查询参数更改时是否自动执行
|
|
2774
|
+
- `debounce`: 防抖配置(delay、leading、trailing)
|
|
2775
|
+
- HTTP 请求选项(method、headers、timeout 等)
|
|
1363
2776
|
|
|
1364
|
-
|
|
2777
|
+
**返回:**
|
|
1365
2778
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
-
|
|
1369
|
-
-
|
|
1370
|
-
-
|
|
1371
|
-
-
|
|
2779
|
+
包含以下内容的对象:
|
|
2780
|
+
|
|
2781
|
+
- `loading`: 布尔值,表示获取当前是否正在执行
|
|
2782
|
+
- `result`: 获取的解析值
|
|
2783
|
+
- `error`: 执行期间发生的任何错误
|
|
2784
|
+
- `status`: 当前执行状态
|
|
2785
|
+
- `reset`: 重置获取器状态的函数
|
|
2786
|
+
- `abort`: 中止当前操作的函数
|
|
2787
|
+
- `getQuery`: 获取当前查询参数的函数
|
|
2788
|
+
- `setQuery`: 更新查询参数的函数
|
|
2789
|
+
- `run`: 使用当前查询执行防抖获取的函数
|
|
2790
|
+
- `cancel`: 取消任何待处理防抖执行的函数
|
|
2791
|
+
- `isPending`: 返回布尔值表示防抖执行当前是否待处理的函数
|
|
2792
|
+
|
|
2793
|
+
#### useDebouncedQuery
|
|
2794
|
+
|
|
2795
|
+
```typescript
|
|
2796
|
+
function useDebouncedQuery<Q, R, E = FetcherError>(
|
|
2797
|
+
options: UseDebouncedQueryOptions<Q, R, E>,
|
|
2798
|
+
): UseDebouncedQueryReturn<Q, R, E>;
|
|
2799
|
+
```
|
|
2800
|
+
|
|
2801
|
+
将通用查询执行与防抖相结合,非常适合自定义查询操作,您希望根据查询参数防抖执行。
|
|
1372
2802
|
|
|
1373
2803
|
**类型参数:**
|
|
1374
2804
|
|
|
@@ -1503,9 +2933,7 @@ function usePromiseState<R = unknown, E = unknown>(
|
|
|
1503
2933
|
- `setError`: 设置状态为 ERROR 并提供错误
|
|
1504
2934
|
- `setIdle`: 设置状态为 IDLE
|
|
1505
2935
|
|
|
1506
|
-
###
|
|
1507
|
-
|
|
1508
|
-
#### useRequestId
|
|
2936
|
+
### useRequestId
|
|
1509
2937
|
|
|
1510
2938
|
```typescript
|
|
1511
2939
|
function useRequestId(): UseRequestIdReturn;
|
|
@@ -1576,191 +3004,170 @@ React hook,用于使用 Map-like 接口管理多个 refs,允许通过键动
|
|
|
1576
3004
|
- `RefKey = string | number | symbol`
|
|
1577
3005
|
- `UseRefsReturn<T> extends Iterable<[RefKey, T]>`
|
|
1578
3006
|
|
|
1579
|
-
###
|
|
3007
|
+
### useQuery
|
|
1580
3008
|
|
|
1581
|
-
|
|
3009
|
+
```typescript
|
|
3010
|
+
function useQuery<Q, R, E = FetcherError>(
|
|
3011
|
+
options: UseQueryOptions<Q, R, E>,
|
|
3012
|
+
): UseQueryReturn<Q, R, E>;
|
|
3013
|
+
```
|
|
1582
3014
|
|
|
1583
|
-
|
|
3015
|
+
用于管理基于查询的异步操作的 React hook,具有自动状态管理和执行控制。
|
|
1584
3016
|
|
|
1585
|
-
|
|
1586
|
-
import { useEventSubscription } from '@ahoo-wang/fetcher-react';
|
|
1587
|
-
import { eventBus } from './eventBus';
|
|
3017
|
+
**类型参数:**
|
|
1588
3018
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
handler: {
|
|
1593
|
-
name: 'myEvent',
|
|
1594
|
-
handle: (event) => {
|
|
1595
|
-
console.log('收到事件:', event);
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
});
|
|
3019
|
+
- `Q`: 查询参数的类型
|
|
3020
|
+
- `R`: 结果值的类型
|
|
3021
|
+
- `E`: 错误值的类型(默认为 FetcherError)
|
|
1599
3022
|
|
|
1600
|
-
|
|
1601
|
-
// 如需要,您也可以手动控制订阅
|
|
1602
|
-
const handleToggleSubscription = () => {
|
|
1603
|
-
if (someCondition) {
|
|
1604
|
-
subscribe();
|
|
1605
|
-
} else {
|
|
1606
|
-
unsubscribe();
|
|
1607
|
-
}
|
|
1608
|
-
};
|
|
3023
|
+
**参数:**
|
|
1609
3024
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
3025
|
+
- `options`: 查询的配置选项
|
|
3026
|
+
- `initialQuery`: 初始查询参数
|
|
3027
|
+
- `execute`: 使用给定参数和可选属性执行查询的函数
|
|
3028
|
+
- `autoExecute?`: 是否在挂载和查询更改时自动执行查询
|
|
3029
|
+
- 所有来自 `UseExecutePromiseOptions` 的选项
|
|
1613
3030
|
|
|
1614
|
-
|
|
3031
|
+
**返回值:**
|
|
1615
3032
|
|
|
1616
|
-
|
|
1617
|
-
- **手动控制**: 提供 `subscribe` 和 `unsubscribe` 函数以进行额外控制
|
|
1618
|
-
- **类型安全**: 完全支持 TypeScript,具有泛型事件类型
|
|
1619
|
-
- **错误处理**: 对失败的订阅尝试记录警告
|
|
1620
|
-
- **事件总线集成**: 与 `@ahoo-wang/fetcher-eventbus` TypedEventBus 实例无缝配合
|
|
3033
|
+
包含查询状态和控制函数的对象:
|
|
1621
3034
|
|
|
1622
|
-
|
|
3035
|
+
- `loading`: 布尔值,指示查询当前是否正在执行
|
|
3036
|
+
- `result`: 查询的解析值
|
|
3037
|
+
- `error`: 执行期间发生的任何错误
|
|
3038
|
+
- `status`: 当前执行状态
|
|
3039
|
+
- `execute`: 使用当前参数执行查询的函数
|
|
3040
|
+
- `reset`: 重置 Promise 状态的函数
|
|
3041
|
+
- `abort`: 中止当前操作的函数
|
|
3042
|
+
- `getQuery`: 检索当前查询参数的函数
|
|
3043
|
+
- `setQuery`: 更新查询参数的函数
|
|
1623
3044
|
|
|
1624
|
-
|
|
3045
|
+
### useQueryState
|
|
1625
3046
|
|
|
1626
|
-
|
|
3047
|
+
```typescript
|
|
3048
|
+
function useQueryState<Q>(
|
|
3049
|
+
options: UseQueryStateOptions<Q>,
|
|
3050
|
+
): UseQueryStateReturn<Q>;
|
|
3051
|
+
```
|
|
1627
3052
|
|
|
1628
|
-
|
|
3053
|
+
用于管理查询状态的 React hook,具有自动执行功能。
|
|
1629
3054
|
|
|
1630
|
-
|
|
1631
|
-
import { useSecurity } from '@ahoo-wang/fetcher-react/cosec';
|
|
1632
|
-
import { tokenStorage } from './tokenStorage';
|
|
1633
|
-
import { useNavigate } from 'react-router-dom';
|
|
3055
|
+
**类型参数:**
|
|
1634
3056
|
|
|
1635
|
-
|
|
1636
|
-
const navigate = useNavigate();
|
|
3057
|
+
- `Q`: 查询参数的类型
|
|
1637
3058
|
|
|
1638
|
-
|
|
1639
|
-
onSignIn: () => {
|
|
1640
|
-
// 登录成功后重定向到仪表板
|
|
1641
|
-
navigate('/dashboard');
|
|
1642
|
-
},
|
|
1643
|
-
onSignOut: () => {
|
|
1644
|
-
// 登出后重定向到登录页面
|
|
1645
|
-
navigate('/login');
|
|
1646
|
-
}
|
|
1647
|
-
});
|
|
3059
|
+
**参数:**
|
|
1648
3060
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
3061
|
+
- `options`: hook 的配置选项
|
|
3062
|
+
- `initialQuery`: 要存储和管理的初始查询参数
|
|
3063
|
+
- `autoExecute?`: 是否在查询更改时或组件挂载时自动执行
|
|
3064
|
+
- `execute`: 使用当前查询参数执行的函数
|
|
1652
3065
|
|
|
1653
|
-
|
|
1654
|
-
await signIn(async () => {
|
|
1655
|
-
const response = await fetch('/api/auth/login', {
|
|
1656
|
-
method: 'POST',
|
|
1657
|
-
body: JSON.stringify({ username, password })
|
|
1658
|
-
});
|
|
1659
|
-
return response.json();
|
|
1660
|
-
});
|
|
1661
|
-
};
|
|
3066
|
+
**返回值:**
|
|
1662
3067
|
|
|
1663
|
-
|
|
1664
|
-
return <button onClick={handleSignIn}>登录</button>;
|
|
1665
|
-
}
|
|
3068
|
+
包含以下内容的对象:
|
|
1666
3069
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
<p>欢迎, {currentUser.sub}!</p>
|
|
1670
|
-
<button onClick={signOut}>登出</button>
|
|
1671
|
-
</div>
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
```
|
|
3070
|
+
- `getQuery`: 检索当前查询参数的函数
|
|
3071
|
+
- `setQuery`: 更新查询参数的函数。如果 autoExecute 为 true 会触发执行
|
|
1675
3072
|
|
|
1676
|
-
|
|
3073
|
+
### useMounted
|
|
1677
3074
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
- **类型安全**: 完全支持 TypeScript,具有 CoSec JWT 负载类型
|
|
1682
|
-
- **令牌持久化**: 与 TokenStorage 集成以实现跨会话持久化
|
|
3075
|
+
```typescript
|
|
3076
|
+
function useMounted(): () => boolean;
|
|
3077
|
+
```
|
|
1683
3078
|
|
|
1684
|
-
|
|
3079
|
+
返回检查组件是否仍挂载的函数的 React hook。
|
|
1685
3080
|
|
|
1686
|
-
|
|
3081
|
+
**返回值:**
|
|
1687
3082
|
|
|
1688
|
-
|
|
1689
|
-
import { SecurityProvider } from '@ahoo-wang/fetcher-react';
|
|
1690
|
-
import { tokenStorage } from './tokenStorage';
|
|
1691
|
-
import { useNavigate } from 'react-router-dom';
|
|
3083
|
+
当组件仍挂载时返回 `true`,否则返回 `false` 的函数。
|
|
1692
3084
|
|
|
1693
|
-
|
|
1694
|
-
const navigate = useNavigate();
|
|
3085
|
+
### useForceUpdate
|
|
1695
3086
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
tokenStorage={tokenStorage}
|
|
1699
|
-
onSignIn={() => navigate('/dashboard')}
|
|
1700
|
-
onSignOut={() => navigate('/login')}
|
|
1701
|
-
>
|
|
1702
|
-
<MyApp />
|
|
1703
|
-
</SecurityProvider>
|
|
1704
|
-
);
|
|
1705
|
-
}
|
|
3087
|
+
```typescript
|
|
3088
|
+
function useForceUpdate(): () => void;
|
|
1706
3089
|
```
|
|
1707
3090
|
|
|
1708
|
-
|
|
3091
|
+
返回强制组件重新渲染的函数的 React hook。
|
|
1709
3092
|
|
|
1710
|
-
|
|
1711
|
-
- `onSignIn`: 登录成功时调用的回调函数
|
|
1712
|
-
- `onSignOut`: 登出时调用的回调函数
|
|
1713
|
-
- `children`: 将有权访问安全上下文的子组件
|
|
3093
|
+
**返回值:**
|
|
1714
3094
|
|
|
1715
|
-
|
|
3095
|
+
调用时强制组件重新渲染的函数。
|
|
1716
3096
|
|
|
1717
|
-
|
|
3097
|
+
### useEventSubscription
|
|
1718
3098
|
|
|
1719
|
-
```
|
|
1720
|
-
|
|
3099
|
+
```typescript
|
|
3100
|
+
function useEventSubscription<EVENT = unknown>(
|
|
3101
|
+
options: UseEventSubscriptionOptions<EVENT>,
|
|
3102
|
+
): UseEventSubscriptionReturn;
|
|
3103
|
+
```
|
|
1721
3104
|
|
|
1722
|
-
|
|
1723
|
-
const { currentUser, authenticated, signOut } = useSecurityContext();
|
|
3105
|
+
为类型化事件总线提供 React 接口的 hook。自动管理订阅生命周期,同时提供手动控制功能。
|
|
1724
3106
|
|
|
1725
|
-
|
|
1726
|
-
return <div>请登录</div>;
|
|
1727
|
-
}
|
|
3107
|
+
**类型参数:**
|
|
1728
3108
|
|
|
1729
|
-
|
|
1730
|
-
<div>
|
|
1731
|
-
<p>欢迎, {currentUser.sub}!</p>
|
|
1732
|
-
<button onClick={signOut}>登出</button>
|
|
1733
|
-
</div>
|
|
1734
|
-
);
|
|
1735
|
-
}
|
|
1736
|
-
```
|
|
3109
|
+
- `EVENT`: 事件总线处理的事件类型(默认为 unknown)
|
|
1737
3110
|
|
|
1738
|
-
|
|
3111
|
+
**参数:**
|
|
1739
3112
|
|
|
1740
|
-
-
|
|
1741
|
-
-
|
|
1742
|
-
-
|
|
1743
|
-
- **自动重新渲染**: 当认证状态更改时,组件自动重新渲染
|
|
3113
|
+
- `options`: 订阅的配置选项
|
|
3114
|
+
- `bus`: 要订阅的 TypedEventBus 实例
|
|
3115
|
+
- `handler`: 具有名称和处理方法的事件处理函数
|
|
1744
3116
|
|
|
1745
|
-
|
|
3117
|
+
**返回值:**
|
|
1746
3118
|
|
|
1747
|
-
|
|
3119
|
+
包含以下内容的对象:
|
|
1748
3120
|
|
|
1749
|
-
|
|
3121
|
+
- `subscribe`: 手动订阅事件总线的函数(返回布尔值成功状态)
|
|
3122
|
+
- `unsubscribe`: 手动取消订阅事件总线的函数(返回布尔值成功状态)
|
|
3123
|
+
|
|
3124
|
+
**相关类型:**
|
|
3125
|
+
|
|
3126
|
+
- `UseEventSubscriptionOptions<EVENT>`: 具有 bus 和 handler 属性的配置接口
|
|
3127
|
+
- `UseEventSubscriptionReturn`: 具有 subscribe 和 unsubscribe 方法的返回接口
|
|
3128
|
+
|
|
3129
|
+
### useKeyStorage
|
|
3130
|
+
|
|
3131
|
+
```typescript
|
|
3132
|
+
// 不使用默认值 - 可能返回 null
|
|
1750
3133
|
function useKeyStorage<T>(
|
|
1751
3134
|
keyStorage: KeyStorage<T>,
|
|
1752
3135
|
): [T | null, (value: T) => void];
|
|
3136
|
+
|
|
3137
|
+
// 使用默认值 - 保证非空
|
|
3138
|
+
function useKeyStorage<T>(
|
|
3139
|
+
keyStorage: KeyStorage<T>,
|
|
3140
|
+
defaultValue: T,
|
|
3141
|
+
): [T, (value: T) => void];
|
|
1753
3142
|
```
|
|
1754
3143
|
|
|
1755
|
-
为 KeyStorage
|
|
3144
|
+
为 KeyStorage 实例提供响应式状态管理的 React hook。订阅存储更改并返回当前值以及设置器函数。可选择接受默认值以在存储为空时使用。
|
|
3145
|
+
|
|
3146
|
+
**类型参数:**
|
|
3147
|
+
|
|
3148
|
+
- `T`: 存储在键存储中的值的类型
|
|
1756
3149
|
|
|
1757
3150
|
**参数:**
|
|
1758
3151
|
|
|
1759
|
-
- `keyStorage`: 要订阅和管理的 KeyStorage
|
|
3152
|
+
- `keyStorage`: 要订阅和管理的 KeyStorage 实例。应该是稳定引用(useRef、memo 或模块级实例)
|
|
3153
|
+
- `defaultValue` _(可选)_: 当存储为空时使用的默认值。提供时,hook 保证返回的值永远不会为 null
|
|
1760
3154
|
|
|
1761
3155
|
**返回值:**
|
|
1762
3156
|
|
|
1763
|
-
-
|
|
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
|
+
```
|
|
1764
3171
|
|
|
1765
3172
|
### useImmerKeyStorage
|
|
1766
3173
|
|
|
@@ -1785,11 +3192,11 @@ function useImmerKeyStorage<T>(
|
|
|
1785
3192
|
|
|
1786
3193
|
**类型参数:**
|
|
1787
3194
|
|
|
1788
|
-
- `T`:
|
|
3195
|
+
- `T`: 存储在键存储中的值的类型
|
|
1789
3196
|
|
|
1790
3197
|
**参数:**
|
|
1791
3198
|
|
|
1792
|
-
- `keyStorage`: 要订阅和管理的 KeyStorage
|
|
3199
|
+
- `keyStorage`: 要订阅和管理的 KeyStorage 实例。应该是稳定引用(useRef、memo 或模块级实例)
|
|
1793
3200
|
- `defaultValue` _(可选)_: 当存储为空时使用的默认值。提供时,hook 保证返回的值永远不会为 null
|
|
1794
3201
|
|
|
1795
3202
|
**返回值:**
|
|
@@ -1848,7 +3255,7 @@ function useListQuery<R, FIELDS extends string = string, E = FetcherError>(
|
|
|
1848
3255
|
**参数:**
|
|
1849
3256
|
|
|
1850
3257
|
- `options`: 包含 initialQuery 和 list 函数的配置选项
|
|
1851
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3258
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1852
3259
|
|
|
1853
3260
|
**返回值:**
|
|
1854
3261
|
|
|
@@ -1873,7 +3280,7 @@ function usePagedQuery<R, FIELDS extends string = string, E = unknown>(
|
|
|
1873
3280
|
**参数:**
|
|
1874
3281
|
|
|
1875
3282
|
- `options`: 包含 initialQuery 和 query 函数的配置选项
|
|
1876
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3283
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1877
3284
|
|
|
1878
3285
|
**返回值:**
|
|
1879
3286
|
|
|
@@ -1898,7 +3305,7 @@ function useSingleQuery<R, FIELDS extends string = string, E = unknown>(
|
|
|
1898
3305
|
**参数:**
|
|
1899
3306
|
|
|
1900
3307
|
- `options`: 包含 initialQuery 和 query 函数的配置选项
|
|
1901
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3308
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1902
3309
|
|
|
1903
3310
|
**返回值:**
|
|
1904
3311
|
|
|
@@ -1922,7 +3329,7 @@ function useCountQuery<FIELDS extends string = string, E = FetcherError>(
|
|
|
1922
3329
|
**参数:**
|
|
1923
3330
|
|
|
1924
3331
|
- `options`: 包含 initialQuery 和 execute 函数的配置选项
|
|
1925
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3332
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1926
3333
|
|
|
1927
3334
|
**返回值:**
|
|
1928
3335
|
|
|
@@ -1948,7 +3355,7 @@ function useFetcherCountQuery<FIELDS extends string = string, E = FetcherError>(
|
|
|
1948
3355
|
- `options`: 计数查询的配置选项,包括条件、fetcher 实例和其他查询设置
|
|
1949
3356
|
- `url`: 从中获取计数的 URL
|
|
1950
3357
|
- `initialQuery`: 计数查询的初始条件
|
|
1951
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3358
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1952
3359
|
|
|
1953
3360
|
**返回值:**
|
|
1954
3361
|
|
|
@@ -1979,7 +3386,7 @@ function useFetcherPagedQuery<
|
|
|
1979
3386
|
- `options`: 分页查询的配置选项,包括分页查询参数、fetcher 实例和其他查询设置
|
|
1980
3387
|
- `url`: 从中获取分页数据的 URL
|
|
1981
3388
|
- `initialQuery`: 初始分页查询配置
|
|
1982
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3389
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
1983
3390
|
|
|
1984
3391
|
**返回值:**
|
|
1985
3392
|
|
|
@@ -2010,7 +3417,7 @@ function useFetcherListQuery<
|
|
|
2010
3417
|
- `options`: 列表查询的配置选项,包括列表查询参数、fetcher 实例和其他查询设置
|
|
2011
3418
|
- `url`: 从中获取列表数据的 URL
|
|
2012
3419
|
- `initialQuery`: 初始列表查询配置
|
|
2013
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3420
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
2014
3421
|
|
|
2015
3422
|
**返回值:**
|
|
2016
3423
|
|
|
@@ -2041,7 +3448,7 @@ function useFetcherListStreamQuery<
|
|
|
2041
3448
|
- `options`: 列表流查询的配置选项,包括列表查询参数、fetcher 实例和其他查询设置
|
|
2042
3449
|
- `url`: 从中获取流数据的 URL
|
|
2043
3450
|
- `initialQuery`: 初始列表查询配置
|
|
2044
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3451
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
2045
3452
|
|
|
2046
3453
|
**返回值:**
|
|
2047
3454
|
|
|
@@ -2072,7 +3479,7 @@ function useFetcherSingleQuery<
|
|
|
2072
3479
|
- `options`: 单个查询的配置选项,包括单个查询参数、fetcher 实例和其他查询设置
|
|
2073
3480
|
- `url`: 从中获取单个项目的 URL
|
|
2074
3481
|
- `initialQuery`: 初始单个查询配置
|
|
2075
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3482
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
2076
3483
|
|
|
2077
3484
|
**返回值:**
|
|
2078
3485
|
|
|
@@ -2101,7 +3508,7 @@ function useListStreamQuery<
|
|
|
2101
3508
|
**参数:**
|
|
2102
3509
|
|
|
2103
3510
|
- `options`: 包含 initialQuery 和 listStream 函数的配置选项
|
|
2104
|
-
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为
|
|
3511
|
+
- `autoExecute`: 是否在组件挂载时自动执行查询(默认为 true)
|
|
2105
3512
|
|
|
2106
3513
|
**返回值:**
|
|
2107
3514
|
|