跳到正文
cccskills
免费在 GitHub 公开

frontend-hook-reference

Reference implementation for frontend data hooks (queries and mutations). MUST be loaded when creating or modifying any hook in an api/ directory or any use*.ts file that calls the generated API client.

中文简介正在准备中。当前显示原文说明。

ayunis-core/ayunis-core星标 34(GitHub 34 + cccskills 0)

实测评分 未实测

查看安装方法

包含的文件(1)

  • SKILL.md9.7 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Frontend Hook Reference Implementation

This skill defines the canonical patterns for data-access hooks. Every hook that calls the API MUST follow these patterns.

Utilities

  • extractErrorData from @/shared/api/extract-error-data — extracts { code, message, status, errors } from Axios errors. Throws if the error is not an AxiosError (network failure, cancellation, etc.).
  • showSuccess / showError from @/shared/lib/toast — user-facing toasts.
  • All user-facing strings use useTranslation with the appropriate namespace.

Pattern 1: Mutation Hook (no form)

For simple actions (delete, toggle, assign, unassign).

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { showSuccess, showError } from '@/shared/lib/toast';
import {
  entityControllerDelete,
  getEntityControllerFindAllQueryKey,
} from '@/shared/api/generated/ayunisCoreAPI';
import { useRouter } from '@tanstack/react-router';
import extractErrorData from '@/shared/api/extract-error-data';

interface DeleteEntityParams {
  id: string;
}

export function useDeleteEntity() {
  const { t } = useTranslation('entities');
  const queryClient = useQueryClient();
  const router = useRouter();

  return useMutation({
    mutationFn: async ({ id }: DeleteEntityParams) => {
      await entityControllerDelete(id);
    },
    onSuccess: () => {
      void queryClient.invalidateQueries({
        queryKey: getEntityControllerFindAllQueryKey(),
      });
      void router.invalidate();
      showSuccess(t('delete.success'));
    },
    onError: (error) => {
      try {
        const { code } = extractErrorData(error);
        switch (code) {
          case 'ENTITY_NOT_FOUND':
            showError(t('delete.notFound'));
            break;
          default:
            showError(t('delete.error'));
        }
      } catch {
        // Non-AxiosError (network failure, request cancellation, etc.)
        showError(t('delete.error'));
      }
    },
  });
}

Pattern 2: Mutation Hook (with form)

For create/update operations that use react-hook-form. For the full form validation pattern including field-level backend errors, load the form-validation-pattern skill.

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useTranslation } from 'react-i18next';
import { showSuccess, showError } from '@/shared/lib/toast';
import {
  entityControllerCreate,
  getEntityControllerFindAllQueryKey,
} from '@/shared/api/generated/ayunisCoreAPI';
import { useRouter } from '@tanstack/react-router';
import extractErrorData from '@/shared/api/extract-error-data';

const createEntitySchema = z.object({
  name: z.string().min(1, 'Name is required'),
});

export type CreateEntityData = z.infer<typeof createEntitySchema>;

export function useCreateEntity() {
  const { t } = useTranslation('entities');
  const queryClient = useQueryClient();
  const router = useRouter();

  const form = useForm<CreateEntityData>({
    resolver: zodResolver(createEntitySchema),
    defaultValues: { name: '' },
  });

  const mutation = useMutation({
    mutationFn: async (data: CreateEntityData) => {
      return await entityControllerCreate(data);
    },
    onSuccess: (data) => {
      void queryClient.invalidateQueries({
        queryKey: getEntityControllerFindAllQueryKey(),
      });
      void router.invalidate();
      showSuccess(t('create.success'));
      if (data.id) {
        void router.navigate({ to: '/entities/$id', params: { id: data.id } });
      }
    },
    onError: (error) => {
      try {
        const { code } = extractErrorData(error);
        switch (code) {
          case 'DUPLICATE_ENTITY_NAME':
            showError(t('create.duplicateName'));
            break;
          default:
            showError(t('create.error'));
        }
      } catch {
        showError(t('create.error'));
      }
    },
  });

  const onSubmit = (data: CreateEntityData) => {
    mutation.mutate(data);
  };

  const resetForm = () => {
    form.reset();
  };

  return {
    form,
    onSubmit,
    resetForm,
    isLoading: mutation.isPending,
  };
}

Pattern 3: Query Hook

For data fetching. Query hooks are simpler — error handling happens at render time via the error return value.

import {
  useEntityControllerFindAll,
  getEntityControllerFindAllQueryKey,
} from '@/shared/api/generated/ayunisCoreAPI';

export function useEntities() {
  const { data, isLoading, error, refetch } = useEntityControllerFindAll(
    {},
    {
      query: {
        queryKey: getEntityControllerFindAllQueryKey({}),
      },
    },
  );

  return {
    entities: data?.data ?? [],
    isLoading,
    error,
    refetch,
  };
}

Rules

1. Every onError MUST use extractErrorData and check error codes

This is the most common mistake. Never show a generic error without checking the code first:

// WRONG ✗ — ignores the error code
onError: (error) => {
  try {
    extractErrorData(error);     // ← result thrown away!
    showError(t('create.error'));
  } catch {
    showError(t('create.error'));
  }
}

// WRONG ✗ — no error inspection at all
onError: () => {
  showError(t('update.error'));
}

// CORRECT ✓ — extracts and switches on code
onError: (error) => {
  try {
    const { code } = extractErrorData(error);
    switch (code) {
      case 'ENTITY_NOT_FOUND':
        showError(t('update.notFound'));
        break;
      case 'DUPLICATE_ENTITY_NAME':
        showError(t('update.duplicateName'));
        break;
      default:
        showError(t('update.error'));
    }
  } catch {
    showError(t('update.error'));
  }
}

2. The try/catch in onError is structural, not optional

extractErrorData throws for non-Axios errors (network failures, cancellations). The catch block must always show a generic fallback error.

3. Map backend error codes to specific user messages

Check the module's *.errors.ts file in the backend for the error codes the endpoint can return. Each relevant code should have a corresponding i18n key and toast message.

4. Cache invalidation after mutations

Always invalidate relevant query keys after successful mutations:

onSuccess: () => {
  void queryClient.invalidateQueries({
    queryKey: getEntityControllerFindAllQueryKey(),
  });
  void router.invalidate();
  showSuccess(t('action.success'));
},

Use void for fire-and-forget invalidation. Invalidate both the list query and any detail queries if applicable.

5. Return shape conventions

Mutation hooks without a form return the mutation result directly via useMutation(...).

Mutation hooks with a form return:

return {
  form,         // react-hook-form instance
  onSubmit,     // function to pass to form.handleSubmit
  resetForm,    // resets the form to defaults
  isLoading: mutation.isPending,
};

Query hooks return domain data with loading/error state:

return {
  entities: data?.data ?? [],
  isLoading,
  error,
  refetch,       // optional, if manual refetch is needed
};

6. API calls live in a hook, never inline in a page

Any component that calls the API does so through a hook in the page's api/ directory — including side-effecting actions like file/CSV/PDF exports and downloads. Pages stay declarative: they consume the hook's return ({ data, isLoading, ... } or { exportEntities, isExporting }) and render. Don't inline fetch/blob-download/URL.createObjectURL logic into a page component.

// WRONG ✗ — export logic inlined in the page component
export default function UsersPage() {
  const exportAdmins = async () => {
    const blob = await someControllerExport();
    const url = URL.createObjectURL(blob);
    // ...DOM download dance inside the page...
  };
  return <Button onClick={() => void exportAdmins()}>Export</Button>;
}

// CORRECT ✓ — logic in a hook, page just consumes it
export default function UsersPage() {
  const { exportAdmins, isExporting } = useUserExport();
  return <Button onClick={() => void exportAdmins()} disabled={isExporting}>Export</Button>;
}

7. Always use the generated client from @/shared/api

Never hand-write a request path with axiosInstance.get/post. Import the Orval-generated function (e.g. entityControllerExport) from @/shared/api. The generated client is the single source of truth for URLs, typing, and the request/response contract — hand-written paths drift silently when the backend changes.

// WRONG ✗ — bypasses Orval, hand-written path + untyped response
const blob = await axiosInstance.get('/admin/users/export', { responseType: 'blob' });

// CORRECT ✓ — generated, typed function
import { entityControllerExport } from '@/shared/api';
const blob = await entityControllerExport();

Checklist

When creating or modifying a hook, verify:

  • onError uses extractErrorData and switches on code
  • Non-Axios errors caught with fallback showError
  • Backend error codes mapped to specific i18n messages
  • onSuccess invalidates relevant query keys
  • void used for fire-and-forget invalidateQueries/router.invalidate()
  • User-facing strings go through useTranslation, not hardcoded
  • No API/blob-download logic inlined in a page — it lives in a hook in api/
  • Uses the generated client from @/shared/api, not a hand-written axiosInstance path

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Query Ayunis deployed-environment telemetry in AppSignal — incidents, occurrence counts and error traces via the `appsignal-cli` binary; structured log attributes (user.id, org.id, nestjs.context, custom metadata) via the v2 logs API. Use when investigating a production/staging incident or log data — NOT for local dev logs (see backend-debugging).

中文简介正在准备中。当前显示原文说明。

ayunis-core/ayunis-core342026年10月26日 更新

Ayunis Core backend specifics — module boundaries, key files, and project conventions. Complements the nestjs-hexagonal-backend skill.

中文简介正在准备中。当前显示原文说明。

ayunis-core/ayunis-core342026年10月26日 更新

Frontend development in ayunis-core. Use when creating, modifying, or debugging frontend code (React, Feature-Sliced Design, API client).

中文简介正在准备中。当前显示原文说明。

ayunis-core/ayunis-core342026年10月26日 更新

Debug backend runtime errors (500s, crashes, unexpected behavior). Use when something is broken at runtime — not for writing new code.

中文简介正在准备中。当前显示原文说明。

ayunis-core/ayunis-core342026年10月26日 更新

Shared test fixtures for backend specs — port mock factories, domain builders, and shared IDs in a per-module testing/ folder. Use when a module's *.use-case.spec.ts files repeat the same mock-repository literal, inline entity construction, or hardcoded UUIDs across files.

中文简介正在准备中。当前显示原文说明。

ayunis-core/ayunis-core342026年10月26日 更新

Analyze Cursor Bugbot PR comments through a root-cause lens: is the finding valid, and is the suggested fix the real fix or a bandaid over a deeper problem? Use when the user asks to check, assess, or triage bugbot comments.

中文简介正在准备中。当前显示原文说明。

ayunis-core/ayunis-core342026年10月26日 更新

查看 ayunis-core 的全部技能

报告此技能的问题