agentleFS
landim32landim32/awesome-ai-skills/skills/react-architecture/SKILL.md

react-architecture

Skill1 starsChanged 7 months agoMITView on GitHub

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.

  • Reads credentials
  • Sends data out

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

14 rules. About 0 are common, 6 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Before creating the architecture, you MUST:

    line 10
  2. Distinctive:

    Always create files in this exact order:

    line 19
  3. Very specific:

    Always check result.sucesso: before updating state (API responses use Portuguese keys).

    line 35
  4. Very specific:

    If StatusResult already exists in another types file, import it instead of redefining

    line 104
  5. Very specific:

    Always use getHeaders(true) for authenticated requests

    line 187
  6. Distinctive:

    After insert: reload full list.

    line 314
  7. Very specific:

    Check result.sucesso before updating state

    line 315
  8. Distinctive:

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

    line 336
  9. Very specific:

    Place the new provider as close to <App /> as possible unless it has dependents

    line 354
  10. Distinctive:

    If other contexts will depend on this one, place it above those contexts

    line 355
  11. Distinctive:

    Keep related contexts grouped (economy, gameplay, admin, etc.)

    line 356
  12. Very specific:

    StatusResult may already exist: Check src/types/map.ts or other type files before redefining.

    line 397
  13. Very specific:

    Entity ID naming: Always use {entity}Id (camelCase), not {entity}_id or id.

    line 398
  14. Very specific:

    Service constructor config: Always include onUnauthorized callback in config interface.

    line 400

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
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:

  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. Types → src/types/{entity}.ts
  2. Service → src/services/{entity}Service.ts
  3. Context → src/contexts/{Entity}Context.tsx
  4. Hook → src/hooks/use{Entity}.ts
  5. Provider registration → src/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

/** {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

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.

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

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:

  • 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:

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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.