con un clic
frontend-query-hooks
Use when creating or using TanStack Query hooks for data fetching
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Use when creating or using TanStack Query hooks for data fetching
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
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
Use when writing or reviewing JavaScript/TypeScript code for style patterns like concise arrows, inline handlers, expression formatting, or when tempted to use eslint-disable
Use when working with environment variables in frontend code
Use when creating or modifying keyboard shortcuts/hotkeys in frontend code
| 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) {
console.error(sb_FromProjects_Select.error);
message.error("Failed to fetch projects!");
throw sb_FromProjects_Select.error;
}
return sb_FromProjects_Select.data;
},
});
const projects = useMemo(() => query.data || [], [query.data]);
const projectsMap = useMemo(
() =>
projects.reduce(
(acc, p) => ({ ...acc, [p.id]: p }),
{} as Record<string, PageOrganization_Projects_QueryData[number]>
),
[projects]
);
return { 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()