feat: add SWR for data fetching with optimistic updates
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { getRecords, createRecord, updateRecord, deleteRecord } from '../lib/api';
|
||||||
|
|
||||||
|
export function useRecords(month, type = 'all') {
|
||||||
|
const key = month ? ['records', month, type] : null;
|
||||||
|
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
key,
|
||||||
|
() => getRecords(month, type),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
dedupingInterval: 5000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 乐观更新:添加记录
|
||||||
|
const addRecord = async (recordData) => {
|
||||||
|
const newRecord = await createRecord(recordData);
|
||||||
|
mutate(
|
||||||
|
(current) => (current ? [...current, newRecord] : [newRecord]),
|
||||||
|
{ revalidate: false }
|
||||||
|
);
|
||||||
|
return newRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 乐观更新:更新记录
|
||||||
|
const editRecord = async (id, recordData) => {
|
||||||
|
await updateRecord(id, recordData);
|
||||||
|
mutate(
|
||||||
|
(current) =>
|
||||||
|
current?.map((r) => (r.id === id ? { ...r, ...recordData } : r)),
|
||||||
|
{ revalidate: false }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 乐观更新:删除记录
|
||||||
|
const removeRecord = async (id) => {
|
||||||
|
await deleteRecord(id);
|
||||||
|
mutate(
|
||||||
|
(current) => current?.filter((r) => r.id !== id),
|
||||||
|
{ revalidate: false }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: data || [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
addRecord,
|
||||||
|
editRecord,
|
||||||
|
removeRecord,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { getStats, getTrendStats, getCategoryStats } from '../lib/api';
|
||||||
|
|
||||||
|
export function useStats(month) {
|
||||||
|
const key = month ? ['stats', month] : null;
|
||||||
|
|
||||||
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
key,
|
||||||
|
() => getStats(month),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: data || { income: 0, expense: 0, balance: 0 },
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTrendStats(period = 'month', month) {
|
||||||
|
const key = ['trend', period, month];
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(
|
||||||
|
key,
|
||||||
|
() => getTrendStats(period, month),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
trend: data || [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCategoryStats(month, type = 'all') {
|
||||||
|
const key = ['categoryStats', month, type];
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(
|
||||||
|
key,
|
||||||
|
() => getCategoryStats(month, type),
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
categories: data || [],
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { getRecords, getStats, getCategories, getRecurringBills } from './api';
|
||||||
|
|
||||||
|
// 全局 SWR 配置
|
||||||
|
export const swrConfig = {
|
||||||
|
refreshInterval: 0, // 默认不自动刷新
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
revalidateOnReconnect: true,
|
||||||
|
dedupingInterval: 2000,
|
||||||
|
errorRetryCount: 3,
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('SWR Error:', error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 数据获取器映射
|
||||||
|
export const fetchers = {
|
||||||
|
records: (month, type) => getRecords(month, type),
|
||||||
|
stats: (month) => getStats(month),
|
||||||
|
categories: () => getCategories(),
|
||||||
|
recurring: () => getRecurringBills(),
|
||||||
|
};
|
||||||
Generated
+33
-1
@@ -11,7 +11,8 @@
|
|||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"next": "^14.0.0",
|
"next": "^14.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"swr": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@kurkle/color": {
|
"node_modules/@kurkle/color": {
|
||||||
@@ -235,6 +236,15 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -435,11 +445,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swr": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"next": "^14.0.0",
|
"next": "^14.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"swr": "^2.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user