AgentSkillsCN

react-arch

为 React 应用中的新实体构建完整的前端架构。生成 TypeScript 类型、服务类、上下文提供者、自定义 Hook,并在 main.tsx 中注册提供者。适用于用户希望在前端创建新实体、功能模块或领域模块时使用此技能。

SKILL.md
--- frontmatter
name: react-arch
description: Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.

This skill defines the standard approach for scaffolding a complete frontend entity architecture in this project. It creates all necessary files following the established patterns: Types, Service, Context, Hook, and Provider registration.

Prerequisites

Before creating the architecture, you MUST:

  1. Ask the user for the entity name (e.g., "Clan", "Achievement", "Shop")
  2. Ask the user for the API base path (e.g., "/api/clan", "/api/achievement")
  3. Ask the user for the entity fields/properties or check if API documentation exists in docs/
  4. Check if the entity already exists — search src/types/, src/services/, src/contexts/, and src/hooks/ before creating anything

Creation Order

Always create files in this exact order:

  1. Typessrc/types/{entity}.ts
  2. Servicesrc/services/{entity}Service.ts
  3. Contextsrc/contexts/{Entity}Context.tsx
  4. Hooksrc/hooks/use{Entity}.ts
  5. Provider registrationsrc/main.tsx

Rules

  1. Never use any type — all types must be explicitly defined.
  2. Never use alert() or window.confirm() — use toast from sonner for notifications and ConfirmModal for confirmation dialogs.
  3. Always use useCallback for all context methods to prevent unnecessary re-renders.
  4. Always include loading, error, and main data state in every context.
  5. Always use the class-based service pattern with private handleResponse method.
  6. Always use getHeaders(true) from apiHelpers for authenticated requests.
  7. Always check result.sucesso before updating state (API responses use Portuguese keys).
  8. Always export both the singleton instance and the class from services.
  9. Always export the context as default and the provider as named export.
  10. Always add JSDoc comments to service methods and type interfaces.

Step 1: Create Types

File: src/types/{entity}.ts

typescript
/** {Entity} Types — Types for the {entity description} system */

// Enums (if needed)
export enum {Entity}StatusEnum {
  Unknown = 0,
  Active = 1,
  Inactive = 2,
}

// Core Entity
/** Main {entity} information */
export interface {Entity}Info {
  {entity}Id: number;
  name: string;
  // ... other fields with JSDoc comments
}

// DTOs
/** Data required to create a new {entity} (no ID) */
export interface {Entity}InsertInfo {
  name: string;
  // ... fields required for creation
}

/** Data required to update an existing {entity} (includes ID) */
export interface {Entity}UpdateInfo {
  {entity}Id: number;
  name: string;
  // ... fields required for update
}

// API Response Types — always include sucesso, mensagem, erros (Portuguese keys)
export interface {Entity}ListResult {
  {entities}: {Entity}Info[];
  sucesso: boolean;
  mensagem: string | null;
  erros: string[] | null;
}

export interface {Entity}GetResult {
  {entity}: {Entity}Info;
  sucesso: boolean;
  mensagem: string | null;
  erros: string[] | null;
}

/** Status-only operation result (import from existing types file if already defined) */
export interface StatusResult {
  sucesso: boolean;
  mensagem: string;
  erros: string[] | null;
}

Key conventions:

  • API responses always have sucesso, mensagem, erros — Portuguese keys
  • Entity IDs use camelCase: {entity}Id (e.g., clanId)
  • Nullable fields use | null, not optional ?
  • Separate interfaces for Insert (no ID) and Update (with ID) DTOs
  • If StatusResult already exists in another types file, import it instead of redefining

Step 2: Create Service

File: src/services/{entity}Service.ts

typescript
import type {
  {Entity}ListResult, {Entity}GetResult,
  {Entity}InsertInfo, {Entity}UpdateInfo, StatusResult,
} from '../types/{entity}';
import { getHeaders } from './apiHelpers';

const API_BASE = `${import.meta.env.VITE_GOBLIN_API_URL || 'http://localhost:4041'}/api/{entity-kebab}`;

interface {Entity}ServiceConfig {
  onUnauthorized?: () => void;
}

/** {Entity} Service — Manages all API operations related to {entities} */
class {Entity}Service {
  private config: {Entity}ServiceConfig;

  constructor(config: {Entity}ServiceConfig = {}) {
    this.config = config;
  }

  private async handleResponse<T>(response: Response): Promise<T> {
    if (response.status === 401) {
      this.config.onUnauthorized?.();
      throw new Error('Unauthorized');
    }
    if (!response.ok) {
      const error = await response.text();
      throw new Error(error || 'Request failed');
    }
    return response.json();
  }

  /** List all {entities} */
  async list(): Promise<{Entity}ListResult> {
    const response = await fetch(`${API_BASE}/list`, { headers: getHeaders(true) });
    return this.handleResponse<{Entity}ListResult>(response);
  }

  /** Get a {entity} by ID */
  async getById(id: number): Promise<{Entity}GetResult> {
    const response = await fetch(`${API_BASE}/getbyid/${id}`, { headers: getHeaders(true) });
    return this.handleResponse<{Entity}GetResult>(response);
  }

  /** Create a new {entity} */
  async insert(data: {Entity}InsertInfo): Promise<{Entity}GetResult> {
    const response = await fetch(`${API_BASE}/insert`, {
      method: 'POST', headers: getHeaders(true), body: JSON.stringify(data),
    });
    return this.handleResponse<{Entity}GetResult>(response);
  }

  /** Update an existing {entity} */
  async update(data: {Entity}UpdateInfo): Promise<{Entity}GetResult> {
    const response = await fetch(`${API_BASE}/update`, {
      method: 'PUT', headers: getHeaders(true), body: JSON.stringify(data),
    });
    return this.handleResponse<{Entity}GetResult>(response);
  }

  /** Delete a {entity} by ID */
  async delete(id: number): Promise<StatusResult> {
    const response = await fetch(`${API_BASE}/delete/${id}`, {
      method: 'DELETE', headers: getHeaders(true),
    });
    return this.handleResponse<StatusResult>(response);
  }
}

export const {entity}Service = new {Entity}Service();
export default {Entity}Service;

Key conventions:

  • Class-based with private handleResponse that checks 401 and calls onUnauthorized
  • API_BASE uses VITE_GOBLIN_API_URL env var with http://localhost:4041 fallback
  • Always use getHeaders(true) for authenticated requests
  • Export both singleton instance (camelCase) and class (PascalCase default)

Step 3: Create Context

File: src/contexts/{Entity}Context.tsx

The context wraps the service and provides state management with three method categories: API Methods (direct service wrappers returning API results), State Management (handle loading/error, update local state), and optionally Utility Methods.

typescript
import { createContext, useState, useCallback, ReactNode } from 'react';
import { {entity}Service } from '../services/{entity}Service';
import type {
  {Entity}Info, {Entity}ListResult, {Entity}GetResult,
  {Entity}InsertInfo, {Entity}UpdateInfo, StatusResult,
} from '../types/{entity}';

interface {Entity}ContextType {
  // State
  {entities}: {Entity}Info[];
  selected{Entity}: {Entity}Info | null;
  loading: boolean;
  error: string | null;
  // API Methods (return API results for caller to check sucesso)
  list{Entities}: () => Promise<{Entity}ListResult>;
  get{Entity}ById: (id: number) => Promise<{Entity}GetResult>;
  insert{Entity}: (data: {Entity}InsertInfo) => Promise<{Entity}GetResult>;
  update{Entity}: (data: {Entity}UpdateInfo) => Promise<{Entity}GetResult>;
  delete{Entity}: (id: number) => Promise<StatusResult>;
  // State Management (return void, handle loading/error internally)
  load{Entities}: () => Promise<void>;
  refresh{Entities}: () => Promise<void>;
  setSelected{Entity}: (item: {Entity}Info | null) => void;
  clearError: () => void;
}

const {Entity}Context = createContext<{Entity}ContextType | undefined>(undefined);

export const {Entity}Provider = ({ children }: { children: ReactNode }) => {
  const [{entities}, set{Entities}] = useState<{Entity}Info[]>([]);
  const [selected{Entity}, setSelected{Entity}] = useState<{Entity}Info | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleError = (err: unknown): never => {
    const errorMsg = err instanceof Error ? err.message : 'Unknown error';
    setError(errorMsg);
    throw err;
  };

  // --- API Methods (direct service wrappers) ---

  const list{Entities} = useCallback(async (): Promise<{Entity}ListResult> => {
    try { setError(null); return await {entity}Service.list(); }
    catch (err) { return handleError(err); }
  }, []);

  const get{Entity}ById = useCallback(async (id: number): Promise<{Entity}GetResult> => {
    try { setError(null); return await {entity}Service.getById(id); }
    catch (err) { return handleError(err); }
  }, []);

  const insert{Entity} = useCallback(async (data: {Entity}InsertInfo): Promise<{Entity}GetResult> => {
    try {
      setError(null);
      const result = await {entity}Service.insert(data);
      if (result.sucesso) await load{Entities}();
      return result;
    } catch (err) { return handleError(err); }
  }, []);

  const update{Entity} = useCallback(async (data: {Entity}UpdateInfo): Promise<{Entity}GetResult> => {
    try {
      setError(null);
      const result = await {entity}Service.update(data);
      if (result.sucesso) {
        set{Entities}((prev) => prev.map((item) =>
          item.{entity}Id === data.{entity}Id ? result.{entity} : item
        ));
        if (selected{Entity}?.{entity}Id === data.{entity}Id) setSelected{Entity}(result.{entity});
      }
      return result;
    } catch (err) { return handleError(err); }
  }, [selected{Entity}]);

  const delete{Entity} = useCallback(async (id: number): Promise<StatusResult> => {
    try {
      setError(null);
      const result = await {entity}Service.delete(id);
      if (result.sucesso) {
        set{Entities}((prev) => prev.filter((item) => item.{entity}Id !== id));
        if (selected{Entity}?.{entity}Id === id) setSelected{Entity}(null);
      }
      return result;
    } catch (err) { return handleError(err); }
  }, [selected{Entity}]);

  // --- State Management ---

  const load{Entities} = useCallback(async (): Promise<void> => {
    try {
      setLoading(true);
      setError(null);
      const result = await {entity}Service.list();
      if (result.sucesso) set{Entities}(result.{entities});
      else throw new Error(result.mensagem || 'Failed to load {entities}');
    } catch (err) { handleError(err); }
    finally { setLoading(false); }
  }, []);

  const refresh{Entities} = useCallback(async () => { await load{Entities}(); }, [load{Entities}]);
  const clearError = useCallback(() => { setError(null); }, []);

  const value: {Entity}ContextType = {
    {entities}, selected{Entity}, loading, error,
    list{Entities}, get{Entity}ById, insert{Entity}, update{Entity}, delete{Entity},
    load{Entities}, refresh{Entities}, setSelected{Entity}, clearError,
  };

  return <{Entity}Context.Provider value={value}>{children}</{Entity}Context.Provider>;
};

export default {Entity}Context;

Key conventions:

  • handleError sets error state AND re-throws (returns never)
  • After insert: reload full list. After update: update item in local state. After delete: remove from local state.
  • Check result.sucesso before updating state
  • Export provider as named export, context as default export

Step 4: Create Hook

File: src/hooks/use{Entity}.ts

typescript
import { useContext } from 'react';
import {Entity}Context from '../contexts/{Entity}Context';

/** Custom hook to access the {Entity} context. Throws if used outside {Entity}Provider. */
export const use{Entity} = () => {
  const context = useContext({Entity}Context);
  if (!context) throw new Error('use{Entity} must be used within a {Entity}Provider');
  return context;
};

export default use{Entity};

Only add computed values/derived state if the user specifically requests it.

Step 5: Register Provider in main.tsx

File: src/main.tsx

typescript
// Add import at the top with other provider imports
import { {Entity}Provider } from './contexts/{Entity}Context.tsx'

// Add to the provider chain based on dependencies:
// - If it depends on AuthContext → must be inside AuthProvider
// - If it depends on GoblinContext → must be inside GoblinProvider
// - If independent → place near the end, before App

Nesting rules:

  • AuthProvider is always the outermost (all contexts depend on auth)
  • Place the new provider as close to <App /> as possible unless it has dependents
  • If other contexts will depend on this one, place it above those contexts
  • Keep related contexts grouped (economy, gameplay, admin, etc.)

Current provider order:

code
AuthProvider → FinanceProvider → GoblinProvider → TeamProvider →
GoboxProvider → AuctionProvider → GLogProvider → QuestProvider →
ItemProvider → ItemClaimProvider → MiningProvider → MapProvider →
TerritoryProvider → TerritoryEnemyProvider → ArenaProvider → App

Naming Convention Reference

ItemConventionExample
Types filesrc/types/{entity}.tssrc/types/clan.ts
Service filesrc/services/{entity}Service.tssrc/services/clanService.ts
Service class{Entity}ServiceClanService
Service instance{entity}ServiceclanService
Context filesrc/contexts/{Entity}Context.tsxsrc/contexts/ClanContext.tsx
Provider{Entity}ProviderClanProvider
Hook filesrc/hooks/use{Entity}.tssrc/hooks/useClan.ts
Hook functionuse{Entity}useClan
Entity ID field{entity}IdclanId
List/Get result{Entity}ListResult / {Entity}GetResultClanListResult / ClanGetResult
Insert/Update DTO{Entity}InsertInfo / {Entity}UpdateInfoClanInsertInfo / ClanUpdateInfo

Verification Checklist

  • Types file at src/types/{entity}.ts with all interfaces
  • Service file at src/services/{entity}Service.ts with class pattern
  • Context file at src/contexts/{Entity}Context.tsx with provider
  • Hook file at src/hooks/use{Entity}.ts with null-check
  • Provider imported and added to src/main.tsx in correct nesting position
  • All API response types include sucesso, mensagem, erros fields
  • All context methods use useCallback
  • handleError pattern: sets error state + re-throws
  • No any types, no alert(), no window.confirm()
  • Service uses getHeaders(true) and VITE_GOBLIN_API_URL env var

Common Gotchas

  • StatusResult may already exist: Check src/types/map.ts or other type files before redefining. Import from existing file if available.
  • Entity ID naming: Always use {entity}Id (camelCase), not {entity}_id or id.
  • API response keys are Portuguese: sucesso (not success), mensagem (not message), erros (not errors).
  • Service constructor config: Always include onUnauthorized callback in config interface.
  • Provider nesting order matters: A context cannot use hooks from providers nested inside it.