@ahoo-wang/fetcher-react 3.1.9 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -5
- package/README.zh-CN.md +242 -355
- package/dist/index.es.js +258 -234
- 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/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/useImmerKeyStorage.d.ts +4 -0
- package/dist/storage/useImmerKeyStorage.d.ts.map +1 -0
- package/dist/storage/useKeyStorage.d.ts +2 -29
- package/dist/storage/useKeyStorage.d.ts.map +1 -1
- package/package.json +4 -2
package/README.zh-CN.md
CHANGED
|
@@ -37,12 +37,13 @@
|
|
|
37
37
|
- [useLatest Hook](#uselatest-hook)
|
|
38
38
|
- [useRefs Hook](#userefs-hook)
|
|
39
39
|
- [useKeyStorage Hook](#usekeystorage-hook)
|
|
40
|
+
- [useImmerKeyStorage Hook](#useimmerkeystorage-hook)
|
|
40
41
|
- [Wow 查询 Hooks](#wow-查询-hooks)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
- [useListQuery Hook](#uselistquery-hook)
|
|
43
|
+
- [usePagedQuery Hook](#usepagedquery-hook)
|
|
44
|
+
- [useSingleQuery Hook](#usesinglequery-hook)
|
|
45
|
+
- [useCountQuery Hook](#usecountquery-hook)
|
|
46
|
+
- [useListStreamQuery Hook](#useliststreamquery-hook)
|
|
46
47
|
- [最佳实践](#最佳实践)
|
|
47
48
|
- [API 参考](#api-参考)
|
|
48
49
|
- [许可证](#许可证)
|
|
@@ -98,443 +99,262 @@ const MyComponent = () => {
|
|
|
98
99
|
};
|
|
99
100
|
```
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
### useImmerKeyStorage Hook
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
import { useListQuery } from '@ahoo-wang/fetcher-react';
|
|
105
|
-
|
|
106
|
-
const MyComponent = () => {
|
|
107
|
-
const { result, loading, error, execute, setCondition } = useListQuery({
|
|
108
|
-
initialQuery: { condition: {}, projection: {}, sort: [], limit: 10 },
|
|
109
|
-
execute: async (listQuery) => fetchListData(listQuery),
|
|
110
|
-
autoExecute: true, // 在组件挂载时自动执行
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// 查询将在组件挂载时自动执行
|
|
114
|
-
// 您仍然可以通过 execute() 手动触发或更新条件
|
|
104
|
+
🚀 **Immer 驱动的不可变状态管理** - `useImmerKeyStorage` hook 通过集成 Immer 的 `produce` 函数扩展了 `useKeyStorage`,允许开发者以直观的"可变"方式更新存储值,同时在底层保持不可变性。非常适合复杂对象的操作,具有自动存储同步功能。
|
|
115
105
|
|
|
116
|
-
|
|
117
|
-
if (error) return <div>错误: {error.message}</div>;
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<div>
|
|
121
|
-
<ul>
|
|
122
|
-
{result?.map((item, index) => (
|
|
123
|
-
<li key={index}>{item.name}</li>
|
|
124
|
-
))}
|
|
125
|
-
</ul>
|
|
126
|
-
</div>
|
|
127
|
-
);
|
|
128
|
-
};
|
|
129
|
-
```
|
|
106
|
+
#### 主要优势
|
|
130
107
|
|
|
131
|
-
|
|
108
|
+
- **直观的变更语法**: 编写看起来可变的代码,但产生不可变更新
|
|
109
|
+
- **深度对象支持**: 轻松处理嵌套对象和数组
|
|
110
|
+
- **类型安全**: 完整的 TypeScript 支持和编译时错误检查
|
|
111
|
+
- **性能优化**: 利用 Immer 的结构共享和最小化重渲染
|
|
112
|
+
- **自动同步**: 变更自动持久化到存储并跨组件同步
|
|
132
113
|
|
|
133
|
-
|
|
114
|
+
#### 使用场景
|
|
134
115
|
|
|
135
|
-
|
|
116
|
+
在需要以下情况时选择 `useImmerKeyStorage` 而不是 `useKeyStorage`:
|
|
136
117
|
|
|
137
|
-
|
|
118
|
+
- 更新嵌套对象属性
|
|
119
|
+
- 执行复杂的数组操作(push、splice 等)
|
|
120
|
+
- 原子性地进行多个相关变更
|
|
121
|
+
- 处理深度嵌套的数据结构
|
|
138
122
|
|
|
139
123
|
```typescript jsx
|
|
140
|
-
import {
|
|
141
|
-
|
|
142
|
-
const SearchComponent = () => {
|
|
143
|
-
const { run: debouncedSearch, cancel, isPending } = useDebouncedCallback(
|
|
144
|
-
async (query: string) => {
|
|
145
|
-
const response = await fetch(`/api/search?q=${query}`);
|
|
146
|
-
const results = await response.json();
|
|
147
|
-
console.log('搜索结果:', results);
|
|
148
|
-
},
|
|
149
|
-
{ delay: 300 }
|
|
150
|
-
);
|
|
124
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
125
|
+
import { useImmerKeyStorage } from '@ahoo-wang/fetcher-react';
|
|
151
126
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
127
|
+
const MyComponent = () => {
|
|
128
|
+
const prefsStorage = new KeyStorage<{
|
|
129
|
+
theme: string;
|
|
130
|
+
volume: number;
|
|
131
|
+
notifications: boolean;
|
|
132
|
+
shortcuts: { [key: string]: string };
|
|
133
|
+
}>({
|
|
134
|
+
key: 'user-prefs'
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 不使用默认值 - 可能为 null
|
|
138
|
+
const [prefs, updatePrefs, clearPrefs] = useImmerKeyStorage(prefsStorage);
|
|
159
139
|
|
|
160
140
|
return (
|
|
161
141
|
<div>
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
142
|
+
<p>主题: {prefs?.theme || '默认'}</p>
|
|
143
|
+
<button onClick={() => updatePrefs(draft => { draft.theme = 'dark'; })}>
|
|
144
|
+
切换到深色主题
|
|
145
|
+
</button>
|
|
146
|
+
<button onClick={() => updatePrefs(draft => { draft.volume += 10; })}>
|
|
147
|
+
增加音量
|
|
148
|
+
</button>
|
|
149
|
+
<button onClick={clearPrefs}>
|
|
150
|
+
清除偏好设置
|
|
151
|
+
</button>
|
|
168
152
|
</div>
|
|
169
153
|
);
|
|
170
154
|
};
|
|
171
155
|
```
|
|
172
156
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
- `delay`: 执行前的延迟毫秒数(必需,正数)
|
|
176
|
-
- `leading`: 第一次调用时立即执行(默认: false)
|
|
177
|
-
- `trailing`: 最后一次调用后延迟执行(默认: true)
|
|
178
|
-
|
|
179
|
-
#### useDebouncedExecutePromise
|
|
180
|
-
|
|
181
|
-
将 Promise 执行与防抖功能相结合,非常适合 API 调用和异步操作。
|
|
157
|
+
#### 使用默认值
|
|
182
158
|
|
|
183
159
|
```typescript jsx
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const { loading, result, error, run } = useDebouncedExecutePromise({
|
|
188
|
-
debounce: { delay: 300 },
|
|
160
|
+
const AudioControls = () => {
|
|
161
|
+
const settingsStorage = new KeyStorage<{ volume: number; muted: boolean }>({
|
|
162
|
+
key: 'audio-settings'
|
|
189
163
|
});
|
|
190
164
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
};
|
|
165
|
+
// 使用默认值 - 保证不为 null
|
|
166
|
+
const [settings, updateSettings, resetSettings] = useImmerKeyStorage(
|
|
167
|
+
settingsStorage,
|
|
168
|
+
{ volume: 50, muted: false }
|
|
169
|
+
);
|
|
197
170
|
|
|
198
171
|
return (
|
|
199
172
|
<div>
|
|
200
|
-
<
|
|
201
|
-
|
|
173
|
+
<p>音量: {settings.volume}%</p>
|
|
174
|
+
<button onClick={() => updateSettings(draft => {
|
|
175
|
+
draft.volume = Math.min(100, draft.volume + 10);
|
|
176
|
+
draft.muted = false;
|
|
177
|
+
})}>
|
|
178
|
+
增加音量
|
|
179
|
+
</button>
|
|
180
|
+
<button onClick={() => updateSettings(draft => { draft.muted = !draft.muted; })}>
|
|
181
|
+
切换静音
|
|
182
|
+
</button>
|
|
183
|
+
<button onClick={resetSettings}>
|
|
184
|
+
重置为默认值
|
|
202
185
|
</button>
|
|
203
|
-
{loading && <div>加载中...</div>}
|
|
204
|
-
{error && <div>错误: {error.message}</div>}
|
|
205
|
-
{result && <div>用户: {result.name}</div>}
|
|
206
186
|
</div>
|
|
207
187
|
);
|
|
208
188
|
};
|
|
209
189
|
```
|
|
210
190
|
|
|
211
|
-
####
|
|
191
|
+
#### 高级用法模式
|
|
212
192
|
|
|
213
|
-
|
|
193
|
+
##### 批量更新
|
|
214
194
|
|
|
215
195
|
```typescript jsx
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
debounce: { delay: 300 },
|
|
222
|
-
onSuccess: (data) => {
|
|
223
|
-
setSearchResults(data.results);
|
|
224
|
-
}
|
|
196
|
+
const updateUserProfile = () => {
|
|
197
|
+
updatePrefs(draft => {
|
|
198
|
+
draft.theme = 'dark';
|
|
199
|
+
draft.notifications = true;
|
|
200
|
+
draft.volume = 75;
|
|
225
201
|
});
|
|
226
|
-
|
|
227
|
-
const handleChange = (value: string) => {
|
|
228
|
-
setQuery(value);
|
|
229
|
-
if (value.trim()) {
|
|
230
|
-
run({
|
|
231
|
-
url: '/api/search',
|
|
232
|
-
method: 'GET',
|
|
233
|
-
params: { q: value }
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<div>
|
|
240
|
-
<input
|
|
241
|
-
value={query}
|
|
242
|
-
onChange={(e) => handleChange(e.target.value)}
|
|
243
|
-
placeholder="搜索..."
|
|
244
|
-
/>
|
|
245
|
-
{loading && <div>搜索中...</div>}
|
|
246
|
-
{error && <div>错误: {error.message}</div>}
|
|
247
|
-
{result && <SearchResults data={result} />}
|
|
248
|
-
</div>
|
|
249
|
-
);
|
|
250
202
|
};
|
|
251
203
|
```
|
|
252
204
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
- **前缘执行**: 第一次调用时立即执行,然后对后续调用进行防抖
|
|
256
|
-
- **后缘执行**: 最后一次调用后延迟执行(默认行为)
|
|
257
|
-
- **前缘 + 后缘**: 立即执行,如果再次调用则在延迟后再次执行
|
|
258
|
-
|
|
259
|
-
### useExecutePromise Hook
|
|
260
|
-
|
|
261
|
-
`useExecutePromise` hook 管理异步操作,具有自动状态处理、竞态条件保护和 promise 状态选项支持。
|
|
205
|
+
##### 数组操作
|
|
262
206
|
|
|
263
207
|
```typescript jsx
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
208
|
+
const todoStorage = new KeyStorage<{
|
|
209
|
+
todos: Array<{ id: number; text: string; done: boolean }>;
|
|
210
|
+
}>({
|
|
211
|
+
key: 'todos',
|
|
212
|
+
});
|
|
268
213
|
|
|
269
|
-
|
|
270
|
-
const response = await fetch('/api/data');
|
|
271
|
-
return response.text();
|
|
272
|
-
};
|
|
214
|
+
const [state, updateState] = useImmerKeyStorage(todoStorage, { todos: [] });
|
|
273
215
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
216
|
+
// 添加新待办事项
|
|
217
|
+
const addTodo = (text: string) => {
|
|
218
|
+
updateState(draft => {
|
|
219
|
+
draft.todos.push({
|
|
220
|
+
id: Date.now(),
|
|
221
|
+
text,
|
|
222
|
+
done: false,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
};
|
|
277
226
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
227
|
+
// 切换待办事项状态
|
|
228
|
+
const toggleTodo = (id: number) => {
|
|
229
|
+
updateState(draft => {
|
|
230
|
+
const todo = draft.todos.find(t => t.id === id);
|
|
231
|
+
if (todo) {
|
|
232
|
+
todo.done = !todo.done;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
};
|
|
282
236
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
<button onClick={handleDirectPromise}>使用 Promise 获取</button>
|
|
289
|
-
<button onClick={reset}>重置</button>
|
|
290
|
-
{result && <p>{result}</p>}
|
|
291
|
-
</div>
|
|
292
|
-
);
|
|
237
|
+
// 清除已完成的待办事项
|
|
238
|
+
const clearCompleted = () => {
|
|
239
|
+
updateState(draft => {
|
|
240
|
+
draft.todos = draft.todos.filter(todo => !todo.done);
|
|
241
|
+
});
|
|
293
242
|
};
|
|
294
243
|
```
|
|
295
244
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
`usePromiseState` hook 提供 promise 操作的状态管理,无执行逻辑。支持静态选项和动态选项供应商。
|
|
245
|
+
##### 嵌套对象更新
|
|
299
246
|
|
|
300
247
|
```typescript jsx
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
248
|
+
const configStorage = new KeyStorage<{
|
|
249
|
+
ui: { theme: string; language: string };
|
|
250
|
+
features: { [key: string]: boolean };
|
|
251
|
+
}>({
|
|
252
|
+
key: 'app-config',
|
|
253
|
+
});
|
|
305
254
|
|
|
306
|
-
|
|
307
|
-
|
|
255
|
+
const [config, updateConfig] = useImmerKeyStorage(configStorage, {
|
|
256
|
+
ui: { theme: 'light', language: 'zh' },
|
|
257
|
+
features: {},
|
|
258
|
+
});
|
|
308
259
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
<p>状态: {status}</p>
|
|
315
|
-
{loading && <p>加载中...</p>}
|
|
316
|
-
{result && <p>结果: {result}</p>}
|
|
317
|
-
{error && <p>错误: {error.message}</p>}
|
|
318
|
-
</div>
|
|
319
|
-
);
|
|
260
|
+
// 更新嵌套属性
|
|
261
|
+
const updateTheme = (theme: string) => {
|
|
262
|
+
updateConfig(draft => {
|
|
263
|
+
draft.ui.theme = theme;
|
|
264
|
+
});
|
|
320
265
|
};
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
#### usePromiseState with Options Supplier
|
|
324
|
-
|
|
325
|
-
```typescript jsx
|
|
326
|
-
import { usePromiseState, PromiseStatus } from '@ahoo-wang/fetcher-react';
|
|
327
266
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
initialStatus: PromiseStatus.IDLE,
|
|
332
|
-
onSuccess: async (result: string) => {
|
|
333
|
-
await saveToAnalytics(result);
|
|
334
|
-
console.log('成功:', result);
|
|
335
|
-
},
|
|
336
|
-
onError: async (error) => {
|
|
337
|
-
await logErrorToServer(error);
|
|
338
|
-
console.error('错误:', error);
|
|
339
|
-
},
|
|
267
|
+
const toggleFeature = (feature: string) => {
|
|
268
|
+
updateConfig(draft => {
|
|
269
|
+
draft.features[feature] = !draft.features[feature];
|
|
340
270
|
});
|
|
341
|
-
|
|
342
|
-
const { setSuccess, setError } = usePromiseState<string>(optionsSupplier);
|
|
343
|
-
|
|
344
|
-
return (
|
|
345
|
-
<div>
|
|
346
|
-
<button onClick={() => setSuccess('动态成功!')}>设置成功</button>
|
|
347
|
-
<button onClick={() => setError(new Error('动态错误!'))}>设置错误</button>
|
|
348
|
-
</div>
|
|
349
|
-
);
|
|
350
271
|
};
|
|
351
272
|
```
|
|
352
273
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
`useRequestId` hook 提供请求ID管理,用于防止异步操作中的竞态条件。
|
|
274
|
+
##### 带验证的条件更新
|
|
356
275
|
|
|
357
276
|
```typescript jsx
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const handleFetch = async () => {
|
|
364
|
-
const requestId = generate();
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
const result = await fetchData();
|
|
368
|
-
|
|
369
|
-
if (isLatest(requestId)) {
|
|
370
|
-
setData(result);
|
|
371
|
-
}
|
|
372
|
-
} catch (error) {
|
|
373
|
-
if (isLatest(requestId)) {
|
|
374
|
-
setError(error);
|
|
375
|
-
}
|
|
277
|
+
const updateVolume = (newVolume: number) => {
|
|
278
|
+
updateSettings(draft => {
|
|
279
|
+
if (newVolume >= 0 && newVolume <= 100) {
|
|
280
|
+
draft.volume = newVolume;
|
|
281
|
+
draft.muted = false; // 音量改变时取消静音
|
|
376
282
|
}
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
return (
|
|
380
|
-
<div>
|
|
381
|
-
<button onClick={handleFetch}>获取数据</button>
|
|
382
|
-
<button onClick={invalidate}>取消进行中</button>
|
|
383
|
-
</div>
|
|
384
|
-
);
|
|
283
|
+
});
|
|
385
284
|
};
|
|
386
285
|
```
|
|
387
286
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
`useLatest` hook 返回包含最新值的 ref 对象,用于在异步回调中访问当前值。
|
|
287
|
+
##### 返回新值
|
|
391
288
|
|
|
392
289
|
```typescript jsx
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const latestCount = useLatest(count);
|
|
398
|
-
|
|
399
|
-
const handleAsync = async () => {
|
|
400
|
-
await someAsyncOperation();
|
|
401
|
-
console.log('最新计数:', latestCount.current); // 始终是最新值
|
|
402
|
-
};
|
|
290
|
+
// 替换整个状态
|
|
291
|
+
const resetToFactorySettings = () => {
|
|
292
|
+
updateSettings(() => ({ volume: 50, muted: false }));
|
|
293
|
+
};
|
|
403
294
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<button onClick={() => setCount(c => c + 1)}>递增</button>
|
|
408
|
-
<button onClick={handleAsync}>异步记录</button>
|
|
409
|
-
</div>
|
|
410
|
-
);
|
|
295
|
+
// 计算更新
|
|
296
|
+
const setMaxVolume = () => {
|
|
297
|
+
updateSettings(draft => ({ ...draft, volume: 100, muted: false }));
|
|
411
298
|
};
|
|
412
299
|
```
|
|
413
300
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
`useRefs` hook 提供 Map-like 接口用于动态管理多个 React refs。它允许通过键注册、检索和管理 refs,并在组件卸载时自动清理。
|
|
301
|
+
##### 错误处理
|
|
417
302
|
|
|
418
303
|
```typescript jsx
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
element?.focus();
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
return (
|
|
430
|
-
<div>
|
|
431
|
-
<div ref={refs.register('first')} tabIndex={0}>第一个元素</div>
|
|
432
|
-
<div ref={refs.register('second')} tabIndex={0}>第二个元素</div>
|
|
433
|
-
<button onClick={() => handleFocus('first')}>聚焦第一个</button>
|
|
434
|
-
<button onClick={() => handleFocus('second')}>聚焦第二个</button>
|
|
435
|
-
</div>
|
|
436
|
-
);
|
|
304
|
+
const safeUpdate = (updater: (draft: any) => void) => {
|
|
305
|
+
try {
|
|
306
|
+
updatePrefs(updater);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('更新偏好设置失败:', error);
|
|
309
|
+
// 适当处理错误
|
|
310
|
+
}
|
|
437
311
|
};
|
|
438
312
|
```
|
|
439
313
|
|
|
440
|
-
|
|
314
|
+
#### 最佳实践
|
|
441
315
|
|
|
442
|
-
|
|
443
|
-
- **Map-like API**: 完整的 Map 接口,包括 get、set、has、delete 等
|
|
444
|
-
- **自动清理**: 组件卸载时自动清空 refs
|
|
445
|
-
- **类型安全**: 完整的 TypeScript 支持
|
|
316
|
+
##### ✅ 推荐做法
|
|
446
317
|
|
|
447
|
-
|
|
318
|
+
- 用于复杂对象更新和数组操作
|
|
319
|
+
- 利用 Immer 的 draft 变更编写可读代码
|
|
320
|
+
- 在单个更新调用中组合多个相关变更
|
|
321
|
+
- 对保证非空状态使用默认值
|
|
322
|
+
- 在更新函数中适当处理错误
|
|
448
323
|
|
|
449
|
-
|
|
324
|
+
##### ❌ 避免做法
|
|
450
325
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const MyComponent = () => {
|
|
456
|
-
const keyStorage = new KeyStorage<string>({ key: 'my-key' });
|
|
326
|
+
- 不要直接用赋值修改 draft 参数(`draft = newValue`)
|
|
327
|
+
- 不要在更新函数中执行副作用
|
|
328
|
+
- 不要依赖对象比较的引用相等性
|
|
329
|
+
- 不要用于简单的原始值更新(应使用 `useKeyStorage`)
|
|
457
330
|
|
|
458
|
-
|
|
459
|
-
const [value, setValue] = useKeyStorage(keyStorage);
|
|
331
|
+
##### 性能提示
|
|
460
332
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
更新值
|
|
466
|
-
</button>
|
|
467
|
-
</div>
|
|
468
|
-
);
|
|
469
|
-
};
|
|
470
|
-
```
|
|
333
|
+
- 将相关更新批量处理以最小化存储操作
|
|
334
|
+
- 当新状态依赖于之前状态时使用函数式更新
|
|
335
|
+
- 如果更新函数经常重新创建,考虑使用 `useCallback`
|
|
336
|
+
- 如果处理非常大的对象,请分析更新性能
|
|
471
337
|
|
|
472
|
-
|
|
338
|
+
##### TypeScript 集成
|
|
473
339
|
|
|
474
340
|
```typescript jsx
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return (
|
|
482
|
-
<div className={theme}>
|
|
483
|
-
<p>当前主题: {theme}</p>
|
|
484
|
-
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
|
485
|
-
切换主题
|
|
486
|
-
</button>
|
|
487
|
-
</div>
|
|
488
|
-
);
|
|
341
|
+
// 为更好的安全性定义严格类型
|
|
342
|
+
type UserPreferences = {
|
|
343
|
+
theme: 'light' | 'dark' | 'auto';
|
|
344
|
+
volume: number; // 0-100
|
|
345
|
+
notifications: boolean;
|
|
346
|
+
shortcuts: Record<string, string>;
|
|
489
347
|
};
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### 更多示例
|
|
493
|
-
|
|
494
|
-
```typescript jsx
|
|
495
|
-
// 处理不同类型的值
|
|
496
|
-
const numberStorage = new KeyStorage<number>({ key: 'counter' });
|
|
497
|
-
const [count, setCount] = useKeyStorage(numberStorage, 0); // 默认为 0
|
|
498
|
-
|
|
499
|
-
// 处理对象
|
|
500
|
-
interface User {
|
|
501
|
-
id: string;
|
|
502
|
-
name: string;
|
|
503
|
-
}
|
|
504
348
|
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// 复杂状态管理
|
|
509
|
-
const settingsStorage = new KeyStorage<{ volume: number; muted: boolean }>({
|
|
510
|
-
key: 'audio-settings',
|
|
511
|
-
});
|
|
512
|
-
const [settings, setSettings] = useKeyStorage(settingsStorage, {
|
|
513
|
-
volume: 50,
|
|
514
|
-
muted: false,
|
|
349
|
+
const prefsStorage = new KeyStorage<UserPreferences>({
|
|
350
|
+
key: 'user-prefs',
|
|
515
351
|
});
|
|
516
352
|
|
|
517
|
-
//
|
|
518
|
-
const
|
|
519
|
-
setSettings({ ...settings, volume: newVolume });
|
|
520
|
-
};
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
### 更多示例
|
|
353
|
+
// TypeScript 将捕获无效更新
|
|
354
|
+
const [prefs, updatePrefs] = useImmerKeyStorage(prefsStorage);
|
|
524
355
|
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
const numberStorage = new KeyStorage<number>({ key: 'counter' });
|
|
528
|
-
const [count, setCount] = useKeyStorage(numberStorage);
|
|
529
|
-
|
|
530
|
-
// 处理对象
|
|
531
|
-
interface User {
|
|
532
|
-
id: string;
|
|
533
|
-
name: string;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const userStorage = new KeyStorage<User>({ key: 'current-user' });
|
|
537
|
-
const [user, setUser] = useKeyStorage(userStorage);
|
|
356
|
+
// 这将导致 TypeScript 错误:
|
|
357
|
+
// updatePrefs(draft => { draft.theme = 'invalid'; });
|
|
538
358
|
```
|
|
539
359
|
|
|
540
360
|
## Wow 查询 Hooks
|
|
@@ -1057,6 +877,73 @@ function useKeyStorage<T>(
|
|
|
1057
877
|
|
|
1058
878
|
- 包含当前存储值和更新函数的元组
|
|
1059
879
|
|
|
880
|
+
### useImmerKeyStorage
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
// 不使用默认值 - 可能返回 null
|
|
884
|
+
function useImmerKeyStorage<T>(
|
|
885
|
+
keyStorage: KeyStorage<T>,
|
|
886
|
+
): [
|
|
887
|
+
T | null,
|
|
888
|
+
(updater: (draft: T | null) => T | null | void) => void,
|
|
889
|
+
() => void,
|
|
890
|
+
];
|
|
891
|
+
|
|
892
|
+
// 使用默认值 - 保证非空
|
|
893
|
+
function useImmerKeyStorage<T>(
|
|
894
|
+
keyStorage: KeyStorage<T>,
|
|
895
|
+
defaultValue: T,
|
|
896
|
+
): [T, (updater: (draft: T) => T | null | void) => void, () => void];
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
为 KeyStorage 实例提供 Immer 驱动的不可变状态管理的 React hook。通过集成 Immer 的 `produce` 函数扩展 `useKeyStorage`,允许直观的"可变"更新存储值,同时保持不可变性。
|
|
900
|
+
|
|
901
|
+
**类型参数:**
|
|
902
|
+
|
|
903
|
+
- `T`: 存储值的数据类型
|
|
904
|
+
|
|
905
|
+
**参数:**
|
|
906
|
+
|
|
907
|
+
- `keyStorage`: 要订阅和管理的 KeyStorage 实例。应该是稳定的引用(useRef、memo 或模块级实例)
|
|
908
|
+
- `defaultValue` _(可选)_: 当存储为空时使用的默认值。提供时,hook 保证返回的值永远不会为 null
|
|
909
|
+
|
|
910
|
+
**返回值:**
|
|
911
|
+
|
|
912
|
+
包含以下元素的元组:
|
|
913
|
+
|
|
914
|
+
- **当前值**: `T | null`(无默认值时)或 `T`(有默认值时)
|
|
915
|
+
- **更新函数**: `(updater: (draft: T | null) => T | null | void) => void` - Immer 驱动的更新函数
|
|
916
|
+
- **清除函数**: `() => void` - 删除存储值的函数
|
|
917
|
+
|
|
918
|
+
**更新函数:**
|
|
919
|
+
|
|
920
|
+
更新函数接收一个可以直接变更的 `draft` 参数。Immer 将从这些变更中产生不可变更新。更新函数也可以直接返回新值或 `null` 来清除存储。
|
|
921
|
+
|
|
922
|
+
**示例:**
|
|
923
|
+
|
|
924
|
+
```typescript
|
|
925
|
+
// 基本对象更新
|
|
926
|
+
const [user, updateUser] = useImmerKeyStorage(userStorage);
|
|
927
|
+
updateUser(draft => {
|
|
928
|
+
if (draft) {
|
|
929
|
+
draft.name = 'John';
|
|
930
|
+
draft.age = 30;
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// 数组操作
|
|
935
|
+
const [todos, updateTodos] = useImmerKeyStorage(todosStorage, []);
|
|
936
|
+
updateTodos(draft => {
|
|
937
|
+
draft.push({ id: 1, text: '新待办事项', done: false });
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// 返回新值
|
|
941
|
+
updateTodos(() => [{ id: 1, text: '重置待办事项', done: false }]);
|
|
942
|
+
|
|
943
|
+
// 清除存储
|
|
944
|
+
updateTodos(() => null);
|
|
945
|
+
```
|
|
946
|
+
|
|
1060
947
|
### useListQuery
|
|
1061
948
|
|
|
1062
949
|
```typescript
|