用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/NTQ78/Todo-app --skill frontend-query-hooks命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | frontend-query-hooks |
| description | Use when creating or using TanStack Query hooks for data fetching |
TanStack Query hook patterns for data fetching and state management.
// ✅ CORRECT - Store entire hook result with short variable name
const qOrganization = useQ_PageOrganization_Organization({ organizationId });
qOrganization.query.isLoading;
qOrganization.organization?.projects;
// ❌ WRONG - Never destructure
const { query, organization } = useQ_PageOrganization_Organization({ organizationId });
// ❌ WRONG - Variable name too verbose
const qPageOrganization_Organization = useQ_PageOrganization_Organization({ organizationId });
Why: Provides namespace, prevents name conflicts, makes refactoring easier.
useQ_[Scope]_[Entity]Scopes:
Page[Name] - Page-level data (e.g., useQ_PageRoot_Organizations)Page[Name]_[SubComp] - Sub-component data (e.g., useQ_PageProjectShot_Files_Files)Me - User-specific data (e.g., useQ_Me)Tables - Direct table access (e.g., useQ_Tables_Organizations)Options - Dropdown/select options from database (e.g., useQ_Options_RolePermissions_Roles)Entity: Plural for lists (Organizations), singular for single items (Organization)
Folder Structure:
hooks/Page[Name]/useQ_Page[Name]_[Entity].tshooks/Page[Name]/Page[Name]_[SubComp]/useQ_Page[Name]_[SubComp]_[Entity].tshooks/Me/useQ_Me.tshooks/Tables/useQ_Tables_[Entity].tshooks/Options/useQ_Options_[Table]_[Field]s.tsq[Entity]const qOrganizations = useQ_PageRoot_Organizations();
const qOrganization = useQ_PageOrganization_Organization({ organizationId });
const qMe = useQ_Me();
const qRoles = useQ_Options_RolePermissions_Roles();
return {
query, // Always return full query object
[rawData], // Raw data array or single item
[derivedMaps], // Optional: Maps for quick lookup
};
Examples: { query, projects, projectsMap } | { query, project } | { query, options, optionsMap }
.list() vs .record()CRITICAL: Use .list() for list queries and .record() for single-record queries. Enables granular invalidation in realtime sync.
import { QueryKeys } from "@/utils/query/queryKeys";
// LIST query - fetches multiple rows, invalidates on ANY table change
queryKey: [...QueryKeys.scenes.list(), { setId }];
// RECORD query - fetches one row by ID, invalidates ONLY when that record changes
queryKey: [...QueryKeys.scenes.record(sceneId)];
// Single record with joins (Case 3: one scene + many files)
queryKey: [
...QueryKeys.scenes.record(sceneId),
...QueryKeys.scene_files.list(),
...QueryKeys.files.list(),
];
| Case | Relationship | Pattern |
|---|---|---|
| Case 1 | A (1) + B (1:1, ID known) | A.record(aId), B.record(bId) |
| Case 2 | As (list) + Bs (list) | A.list(), B.list() |
| Case 3 | A (1) + Bs (1:N array) | A.record(aId), B.list() |
| Case 1b | A (1) + B (1:1, ID unknown) | A.record(aId), B.list() |
Rules:
.list() - fetches multiple rows.record(id) - fetches one row by ID.list() - any change invalidates parent.record() - if you know the joined IDQuery hooks should do MINIMAL data transformation. Let components handle business logic.
query.data || []reduce((acc, item) => ({ ...acc, [item.id]: item }), {})organization.projects → projects)// ✅ CORRECT - Use raw data directly inline
return (
<div>
{qOrganization.organization?.owner_id === qMe.user?.id && <OwnerBadge />}
<h1>{qOrganization.organization?.name}</h1>
</div>
);
// ❌ WRONG - Don't create intermediate variables in hooks
const isOwner = useMemo(() => organization?.owner_id === qMe.user?.id, [...]);
// ✅ Extract const when same check repeats 3+ times
const isCurrentUserOwner = qOrganization.organization?.owner_id === qMe.user?.id;
// ✅ Helper functions for loops with different parameters
const isUserOwner = (userId: string) => qOrganization.organization?.owner_id === userId;
Rules: No useMemo for simple booleans, extract at 3+ repetitions, use helper functions in loops.
import { supabase, QueryData } from "@/configs/supabase/config";
// 1. Extract query function OUTSIDE hook
const createProjectsQuery = (orgId: string) =>
supabase
.from("projects")
.select("id, name, description, created_at") // Explicit columns only!
.eq("organization_id", orgId);
// 2. Infer type from query shape
export type PageOrganization_Projects_QueryData = QueryData<ReturnType<typeof createProjectsQuery>>;
// 3. Use in hook
export const useQ_PageOrganization_Organization = ({ organizationId }: Params) => {
const { message } = useApp();
const query = useQuery({
enabled: !!organizationId,
queryKey: [...QueryKeys.projects.list(), { organizationId }],
queryFn: async () => {
const sb_FromProjects_Select = await createProjectsQuery(organizationId);
if (sb_FromProjects_Select.error) {
.(sb_FromProjects_Select.);
message.();
sb_FromProjects_Select.;
}
sb_FromProjects_Select.;
},
});
projects = ( query. || [], [query.]);
projectsMap = (
projects.(
({ ...acc, [p.]: p }),
{} <, _Projects_QueryData[]>
),
[projects]
);
{ query, projects, projectsMap };
};
Benefits: Compile-time safety, better performance, self-documenting, refactor-safe.
JSONB Columns: Use database override pattern with MergeDeep from type-fest. See supabase-schema-design skill.
Export query key factory when mutations need cache access:
// Export factory (NOT a hook)
export const PageSet_SceneFile_QueryKey = (sceneFileId: string) =>
[...QueryKeys.scene_files.record(sceneFileId), ...QueryKeys.files.list()] as const;
// Use in query hook
queryKey: PageSet_SceneFile_QueryKey(sceneFileId);
// Use in mutation for optimistic updates
import {
PageSet_SceneFile_QueryKey,
type PageSet_SceneFile_QueryData,
} from "./useQ_PageSet_SceneFile";
const queryKey = PageSet_SceneFile_QueryKey(variables.id);
queryClient.setQueryData(queryKey, optimisticValue);
Naming: useQ_PageSet_SceneFile → PageSet_SceneFile_QueryKey
For dropdown/select options from database tables.
useQ_Options_[Table]_[Field]suseQ_Options_RolePermissions_Roles; // Unique roles from role_permissions.role
useQ_Options_Files_MimeTypes; // Unique MIME types from files.mime_type
return {
query, // TanStack Query object
options, // Array<{ label, value }> for Antd Select
optionsMap, // Record<value, { label, value }> for lookups
};
const qRoles = useQ_Options_RolePermissions_Roles();
<Select options={qRoles.options} loading={qRoles.query.isLoading} />
Key patterns: Labels formatted for display, values are raw DB values, use Array.from(new Set(...)) for unique values.
Compose hooks to avoid deeply nested joins:
export const useQ_PageRoot_Organizations = () => {
const qMe = useQ_Me();
const organizationIds = useMemo(
() => qMe.user?.organization_memberships?.map((m) => m.organization_id) || [],
[qMe.user]
);
const qTables_Organizations = useQ_Tables_Organizations({ organization_ids: organizationIds });
// ... combine data
return { query: qMe.query, organizations, organizationsMap };
};
See frontend-profile-batch-query skill for the cross-schema join workaround pattern when fetching multiple user profiles.
Hook Structure:
useQ_[Scope]_[Entity]q[Entity] (drop scope prefix){ query, [data], [maps] } only - no business logicselect("*")).list() or .record())message.error()enabled flag for conditional queriesOptions Hook:
useQ_Options_[Table]_[Field]s{ query, options, optionsMap }Array<{ label: string, value: string }>hooks/Options/select("*") → Use explicit columns with QueryData.list() or .record()Use when deploying Cloudflare Workers, managing R2 storage, or working with Cloudflare infrastructure
Use when working with ANTD components, theme tokens, icons, forms, or feedback components (message/notification/modal)
Use when adding, referencing, or serving static assets (images, fonts, videos, 3D models) through the R2 CDN pipeline with type-safe imports