|
|
@@ -873,6 +873,125 @@ const MyPage: React.FC = () => {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+#### 9.1.4 数据修改 (useMutation)
|
|
|
+
|
|
|
+对于需要修改服务端数据的操作(POST、PUT、DELETE),使用`useMutation`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
+import { apiClient } from '../api'
|
|
|
+
|
|
|
+const MyPage: React.FC = () => {
|
|
|
+ const queryClient = useQueryClient()
|
|
|
+
|
|
|
+ // 数据修改mutation
|
|
|
+ const mutation = useMutation({
|
|
|
+ mutationFn: async (formData: MyFormData) => {
|
|
|
+ const res = await apiClient.resource.$post({
|
|
|
+ json: formData
|
|
|
+ })
|
|
|
+ if (!res.ok) {
|
|
|
+ throw new Error('操作失败')
|
|
|
+ }
|
|
|
+ return await res.json()
|
|
|
+ },
|
|
|
+ onSuccess: (data) => {
|
|
|
+ // 成功后刷新相关查询
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['resource-list'] })
|
|
|
+ Taro.showToast({
|
|
|
+ title: '操作成功',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ Taro.showToast({
|
|
|
+ title: error.message,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const handleSubmit = (formData: MyFormData) => {
|
|
|
+ mutation.mutate(formData)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View>
|
|
|
+ <button onClick={() => handleSubmit(formData)}>
|
|
|
+ {mutation.isPending ? '提交中...' : '提交'}
|
|
|
+ </button>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- 使用`mutationFn`定义异步操作
|
|
|
+- 使用`onSuccess`处理成功逻辑,通常需要`invalidateQueries`刷新数据
|
|
|
+- 使用`onError`处理错误
|
|
|
+- 使用`isPending`判断加载状态
|
|
|
+
|
|
|
+#### 9.1.5 无限滚动查询 (useInfiniteQuery)
|
|
|
+
|
|
|
+对于分页列表数据,使用`useInfiniteQuery`实现无限滚动:
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { useInfiniteQuery } from '@tanstack/react-query'
|
|
|
+import { apiClient } from '../api'
|
|
|
+
|
|
|
+const MyPage: React.FC = () => {
|
|
|
+ // 无限滚动查询
|
|
|
+ const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
|
|
+ queryKey: ['infinite-list'],
|
|
|
+ queryFn: async ({ pageParam = 0 }) => {
|
|
|
+ const res = await apiClient.items.$get({
|
|
|
+ query: { skip: pageParam * 20, take: 20 }
|
|
|
+ })
|
|
|
+ if (!res.ok) {
|
|
|
+ throw new Error('获取数据失败')
|
|
|
+ }
|
|
|
+ const data = await res.json()
|
|
|
+ return {
|
|
|
+ items: data.data || [],
|
|
|
+ nextCursor: pageParam + 1
|
|
|
+ }
|
|
|
+ },
|
|
|
+ initialPageParam: 0,
|
|
|
+ getNextPageParam: (lastPage) => lastPage.nextCursor
|
|
|
+ })
|
|
|
+
|
|
|
+ // 扁平化所有页的数据
|
|
|
+ const allItems = data?.pages.flatMap(page => page.items) || []
|
|
|
+
|
|
|
+ const handleLoadMore = () => {
|
|
|
+ if (hasNextPage && !isFetchingNextPage) {
|
|
|
+ fetchNextPage()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <ScrollView
|
|
|
+ scrollY
|
|
|
+ onScrollToLower={handleLoadMore}
|
|
|
+ >
|
|
|
+ {allItems.map(item => (
|
|
|
+ <View key={item.id}>{item.name}</View>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {isFetchingNextPage && <Text>加载更多...</Text>}
|
|
|
+ {!hasNextPage && allItems.length > 0 && <Text>没有更多数据了</Text>}
|
|
|
+ </ScrollView>
|
|
|
+ )
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**关键点**:
|
|
|
+- `pageParam`用于传递分页参数
|
|
|
+- `getNextPageParam`决定是否有下一页
|
|
|
+- 使用`pages.flatMap`合并所有页数据
|
|
|
+- 使用`fetchNextPage`加载下一页
|
|
|
+- 使用`hasNextPage`和`isFetchingNextPage`控制加载状态
|
|
|
+
|
|
|
## 测试规范
|
|
|
|
|
|
### 10.1 Jest配置
|
|
|
@@ -1020,6 +1139,14 @@ describe('MyPage', () => {
|
|
|
- **原因**: 不同查询使用了相同的queryKey
|
|
|
- **解决**: 为每个查询使用唯一的queryKey
|
|
|
|
|
|
+**问题**: mutation后数据没有更新
|
|
|
+- **原因**: 忘记调用invalidateQueries
|
|
|
+- **解决**: 在onSuccess回调中刷新相关查询
|
|
|
+
|
|
|
+**问题**: 无限滚动一直触发加载
|
|
|
+- **原因**: getNextPageParam返回逻辑错误
|
|
|
+- **解决**: 正确判断是否还有下一页,返回undefined或nextCursor
|
|
|
+
|
|
|
## 最佳实践
|
|
|
|
|
|
### 12.1 组件开发
|
|
|
@@ -1040,7 +1167,23 @@ describe('MyPage', () => {
|
|
|
4. **多个独立查询使用不同的queryKey**
|
|
|
5. **测试中使用真实的QueryClientProvider**
|
|
|
|
|
|
-### 12.3 性能优化
|
|
|
+### 12.3 数据修改
|
|
|
+
|
|
|
+1. **使用useMutation处理POST/PUT/DELETE操作**
|
|
|
+2. **在mutationFn中检查response.ok,失败时throw Error**
|
|
|
+3. **使用onSuccess刷新相关查询(invalidateQueries)**
|
|
|
+4. **使用onError显示错误提示**
|
|
|
+5. **使用isPending显示加载状态,避免重复提交**
|
|
|
+
|
|
|
+### 12.4 无限滚动
|
|
|
+
|
|
|
+1. **使用useInfiniteQuery处理分页列表**
|
|
|
+2. **使用pages.flatMap合并所有页数据**
|
|
|
+3. **正确实现getNextPageParam判断是否有下一页**
|
|
|
+4. **使用hasNextPage和isFetchingNextPage控制加载状态**
|
|
|
+5. **在ScrollView的onScrollToLower中触发fetchNextPage**
|
|
|
+
|
|
|
+### 12.5 性能优化
|
|
|
|
|
|
1. **使用Image组件的lazyLoad属性**
|
|
|
2. **列表数据使用虚拟滚动**
|
|
|
@@ -1048,7 +1191,7 @@ describe('MyPage', () => {
|
|
|
4. **使用React.memo优化组件性能**
|
|
|
5. **利用React Query的缓存机制减少重复请求**
|
|
|
|
|
|
-### 12.4 代码质量
|
|
|
+### 12.6 代码质量
|
|
|
|
|
|
1. **遵循项目编码标准**
|
|
|
2. **编写单元测试和集成测试**
|
|
|
@@ -1079,6 +1222,7 @@ describe('MyPage', () => {
|
|
|
|
|
|
| 版本 | 日期 | 变更说明 | 作者 |
|
|
|
|------|------|----------|------|
|
|
|
+| 1.3 | 2025-12-28 | 添加useMutation和useInfiniteQuery规范,完善React Query最佳实践 | James (Claude Code) |
|
|
|
| 1.2 | 2025-12-28 | 添加React Query数据获取规范,更新测试规范章节 | James (Claude Code) |
|
|
|
| 1.1 | 2025-12-26 | 添加图标使用规范(Heroicons) | Bob (Scrum Master) |
|
|
|
| 1.0 | 2025-12-26 | 基于史诗011和017经验创建Mini UI包开发规范 | Bob (Scrum Master) |
|