Agent skill
react-arch
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/react-arch-emaginebr-nnews-react
SKILL.md
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:
- Ask the user for the entity name (e.g., "Clan", "Achievement", "Shop")
- Ask the user for the API base path (e.g., "/api/clan", "/api/achievement")
- Ask the user for the entity fields/properties or check if API documentation exists in
docs/ - Check if the entity already exists — search
src/types/,src/services/,src/contexts/, andsrc/hooks/before creating anything
Creation Order
Always create files in this exact order:
- Types →
src/types/{entity}.ts - Service →
src/services/{entity}Service.ts - Context →
src/contexts/{Entity}Context.tsx - Hook →
src/hooks/use{Entity}.ts - Provider registration →
src/main.tsx
Rules
- Never use
anytype — all types must be explicitly defined. - Never use
alert()orwindow.confirm()— usetoastfromsonnerfor notifications andConfirmModalfor confirmation dialogs. - Always use
useCallbackfor all context methods to prevent unnecessary re-renders. - Always include
loading,error, and main data state in every context. - Always use the class-based service pattern with private
handleResponsemethod. - Always use
getHeaders(true)fromapiHelpersfor authenticated requests. - Always check
result.sucessobefore updating state (API responses use Portuguese keys). - Always export both the singleton instance and the class from services.
- Always export the context as default and the provider as named export.
- Always add JSDoc comments to service methods and type interfaces.
Step 1: Create Types
File: src/types/{entity}.ts
/** {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
StatusResultalready exists in another types file, import it instead of redefining
Step 2: Create Service
File: src/services/{entity}Service.ts
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
handleResponsethat checks 401 and callsonUnauthorized API_BASEusesVITE_GOBLIN_API_URLenv var withhttp://localhost:4041fallback- 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.
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:
handleErrorsets error state AND re-throws (returnsnever)- After insert: reload full list. After update: update item in local state. After delete: remove from local state.
- Check
result.sucessobefore updating state - Export provider as named export, context as default export
Step 4: Create Hook
File: src/hooks/use{Entity}.ts
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
// 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:
AuthProvideris 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:
AuthProvider → FinanceProvider → GoblinProvider → TeamProvider →
GoboxProvider → AuctionProvider → GLogProvider → QuestProvider →
ItemProvider → ItemClaimProvider → MiningProvider → MapProvider →
TerritoryProvider → TerritoryEnemyProvider → ArenaProvider → App
Naming Convention Reference
| Item | Convention | Example |
|---|---|---|
| Types file | src/types/{entity}.ts |
src/types/clan.ts |
| Service file | src/services/{entity}Service.ts |
src/services/clanService.ts |
| Service class | {Entity}Service |
ClanService |
| Service instance | {entity}Service |
clanService |
| Context file | src/contexts/{Entity}Context.tsx |
src/contexts/ClanContext.tsx |
| Provider | {Entity}Provider |
ClanProvider |
| Hook file | src/hooks/use{Entity}.ts |
src/hooks/useClan.ts |
| Hook function | use{Entity} |
useClan |
| Entity ID field | {entity}Id |
clanId |
| List/Get result | {Entity}ListResult / {Entity}GetResult |
ClanListResult / ClanGetResult |
| Insert/Update DTO | {Entity}InsertInfo / {Entity}UpdateInfo |
ClanInsertInfo / ClanUpdateInfo |
Verification Checklist
- Types file at
src/types/{entity}.tswith all interfaces - Service file at
src/services/{entity}Service.tswith class pattern - Context file at
src/contexts/{Entity}Context.tsxwith provider - Hook file at
src/hooks/use{Entity}.tswith null-check - Provider imported and added to
src/main.tsxin correct nesting position - All API response types include
sucesso,mensagem,errosfields - All context methods use
useCallback -
handleErrorpattern: sets error state + re-throws - No
anytypes, noalert(), nowindow.confirm() - Service uses
getHeaders(true)andVITE_GOBLIN_API_URLenv var
Common Gotchas
- StatusResult may already exist: Check
src/types/map.tsor other type files before redefining. Import from existing file if available. - Entity ID naming: Always use
{entity}Id(camelCase), not{entity}_idorid. - API response keys are Portuguese:
sucesso(notsuccess),mensagem(notmessage),erros(noterrors). - Service constructor config: Always include
onUnauthorizedcallback in config interface. - Provider nesting order matters: A context cannot use hooks from providers nested inside it.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?