React architecture
Claude AI Skills repository
npx -y skills add landim32/awesome-ai-skills --skill react-architectureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
15.2 KB, as published. Nobody here has run it
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.