agentleFS
LongJohnSilver1504LongJohnSilver1504/claude-skills/skills/react-clean-architecture/SKILL.md

react-clean-architecture

Skill1 starsChanged 58 days agoMITView on GitHub

Principles for structuring React applications with clean architecture. Use when refactoring, reviewing code, deciding when to abstract, or understanding layer responsibilities. Covers the WHY behind structure decisions.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 9 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When layers are mixed, a simple UI change requires understanding HTTP logic.

    line 49
  2. Distinctive:

    Create abstractions when they're deep:

    line 112
  3. Distinctive:

    Don't create abstractions when they're shallow:

    line 117
  4. Very specific:

    Prefer early returns over nested conditionals: handle faulty/loading states first (if (!prompt) return <LoadingQuote />), keep the happy …

    line 132
  5. Distinctive:

    Single point of validation: - Don't repeat checks everywhere

    line 169
  6. Distinctive:

    Type safety downstream: - After validation, types are guaranteed

    line 171
  7. Distinctive:

    When APIs are too generic:

    line 210
  8. Distinctive:

    Waterfall requests: - Wait for each response

    line 213
  9. Distinctive:

    Complexity migration: - Not eliminated, just moved

    line 214
  10. Distinctive:

    Don't build a generic API and force the frontend to assemble data.

    line 221

≈ “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

React Clean Architecture Principles

Overview

This skill provides the principles and decision-making framework for building maintainable React applications. It answers WHEN and WHY to structure code, while create-feature provides the HOW.

Core philosophy: Structure exists to manage change. Good architecture makes future changes easy; bad architecture makes them expensive.

Related Skills

SkillRelationship
create-featureImplementation templates for these principles
prd-to-uxUX foundations before building
frontend-testingTesting the structured code

Layer Separation

The Three Layers

┌─────────────────────────────────────────┐
│           PRESENTATION LAYER            │
│  Components, JSX, event handlers        │
│  "What to render"                       │
├─────────────────────────────────────────┤
│           BUSINESS LOGIC LAYER          │
│  Custom hooks, domain services          │
│  "What to do with data"                 │
├─────────────────────────────────────────┤
│           TRANSPORT LAYER               │
│  HTTP clients, API adapters             │
│  "How to get data"                      │
└─────────────────────────────────────────┘

Why Layers Matter

Different layers change at different rates:

  • Transport changes rarely (API endpoints are stable)
  • Business logic changes sometimes (new features, rules)
  • Presentation changes often (UI tweaks, styling)

When layers are mixed, a simple UI change requires understanding HTTP logic. When separated, changes stay contained.

Layer Rules

LayerCan Import FromNever Imports
Transport (api/)Domain schemasReact, hooks, components
Business Logic (hooks/, domain/)Transport, domainComponents
Presentation (components/)Hooks, domain servicesTransport directly
// ❌ Component importing transport layer
import { axios } from 'axios'

const PaymentCard = () => {
  useEffect(() => {
    axios.get('/payments').then(...)  // Layer violation
  }, [])
}

// ✅ Component using hook (which hides transport)
const PaymentCard = () => {
  const { data } = usePayments()  // Clean separation
}

Deep vs Shallow Modules

The Iceberg Principle

A deep module has a small API surface hiding large complexity (like an iceberg).

// Deep module: Simple API, complex internals
const { prompt, handleSubmit } = usePrompt()

// Inside usePrompt: query client, mutations, caching, error handling,
// refetch logic, optimistic updates... all hidden

A shallow module exposes too much, hiding little.

// Shallow module: Large API, simple internals
const {
  data,
  isPending,
  isError,
  error,
  refetch,
  queryKey,
  queryFn,
  staleTime,
  ...
} = usePromptQuery()

// Just wraps useQuery with no added value

When to Abstract

Create abstractions when they're deep:

  • Hide 5+ lines of logic behind 1 function call
  • Encapsulate decisions (caching, error handling)
  • Provide domain-specific names (usePrompt not useQueryWithConfig)

Don't create abstractions when they're shallow:

  • Just renaming existing functions
  • Adding a layer that passes everything through
  • Wrapping with no added behavior

Interface Design for Testability

Interfaces that accept dependencies (instead of creating them) and return results (instead of producing side effects) are testable by construction, and mockable at system boundaries without touching internals. See references/interface-design.md for the patterns: dependency injection at boundaries and SDK-style API adapters over generic fetchers.


Guard Clauses

Prefer early returns over nested conditionals: handle faulty/loading states first (if (!prompt) return <LoadingQuote />), keep the happy path unnested, and invert conditions (if (!x) return ... instead of else). A nested ternary in JSX is a signal the component needs splitting.


Trust Boundary Validation

This pattern has a name: the Anti-Corruption Layer (DDD), implemented as DTO + Mapper, following "Parse, don't validate" (Alexis King). Validate the wire format at the boundary (parseResponse(dtoSchema, data)), map it to a frontend-owned domain model (toX(dto)), and the rest of the app never doubts the data — backend changes stop at api/. The full rule (file layout, import restrictions, detection layers) lives in rules/api-boundary.md.

Validate at the Edge

Validate data where it enters your application, not where it's used.

// ❌ Validation in component
function AnswersList({ answers }) {
  if (!Array.isArray(answers)) return null
  if (!answers.every(a => a.text && a.author)) return null
  // Now render...
}

// ❌ Validation in hook
function usePrompt() {
  const { data } = useQuery(...)
  if (!data?.answers) return { prompt: null }
  // Check every field...
}

// ✅ Validation in transport layer
export function getActivePrompt() {
  const { data } = await client.get('/prompts/active')
  const dto = promptDtoSchema.parse(data)  // 1. validate the wire format
  return toPrompt(dto)                     // 2. map to the domain model
}

Why Edge Validation Works

  1. Single point of validation - Don't repeat checks everywhere
  2. Fail fast - Bad data caught immediately
  3. Type safety downstream - After validation, types are guaranteed
  4. No runtime surprises - Components never see malformed data

TypeScript Casting is Not Validation

// ❌ False security
const data = response.data as Prompt  // No runtime check

// ✅ Actual validation
const data = promptSchema.parse(response.data)  // Runtime + types

Domain-Driven API Design

Design for Access Patterns, Not REST Purity

Build APIs around what the UI needs, not generic REST conventions.

❌ RESTful purism:
GET /prompts                    → Get list
GET /prompts?active=true        → Get current ID
GET /prompts/:id                → Get prompt data
GET /prompts/:id/answers        → Get answers
GET /users/:id/answered/:promptId → Check if answered

= 5 requests, complex client logic

✅ Domain-driven:
GET /prompt/active              → Everything the page needs

= 1 request, simple client

The Purism Trap

When APIs are too generic:

  • Frontend becomes the driver - Business logic leaks to client
  • Overfetching - Get data you won't use
  • Waterfall requests - Wait for each response
  • Complexity migration - Not eliminated, just moved

Backend Decides What, Frontend Decides How

  • Backend: What data to return, what's allowed
  • Frontend: How to render, what to show

Don't build a generic API and force the frontend to assemble data.


Component Responsibility (Principle)

Components are pure renderers — they receive data and return JSX. Logic, state, and side effects belong in hooks.

This skill covers the why. For the project-specific how (file structure, hook naming, wizard step patterns, dialog state naming), see your project's component-hook-separation.md rule.


Where Does This State Live?

Walk the ladder top-down and stop at the first match. In this model the hook owns the state; the component only receives it already shaped.

The state is...It belongs in...Skipping the rung costs you
Server data — anything the API ownsTanStack Query, read through a feature hook. Never copied into a store or seeded into useStateCopying query data into Zustand creates two sources of truth that drift on the next refetch
UI state one component usesuseState inside that component's co-located hookA global store entry for a local toggle survives unmount and leaks stale UI into the next mount
UI state two siblings shareuseState in the nearest common parent's hook, passed down as propsDuplicating it in both siblings means they disagree the moment one of them updates
Client state crossing screens, or updating on every keystroke/dragZustand, read with primitive selectors (useStore((s) => s.step))Context re-renders every consumer on every update; selecting the whole store object re-renders on unrelated changes
Low-frequency global config (theme, locale, auth session)Context, one provider near the app rootA store for values that change once per session adds indirection and hides the dependency from the tree

Server data plus a little client state is the common case — keep them separate (query for the list, store for "which row is selected") instead of merging them into one blob.

Project specifics: tanstack-query.md (query keys, invalidation), zustand-patterns.md (selectors, why the store object breaks dep arrays), component-hook-separation.md (where the hook lives).


useEffect — When NOT to Reach for It

useEffect synchronizes with systems outside React — browser APIs, third-party widgets, real subscriptions. For everything below there is a shorter answer that cannot fall out of sync:

Reaching for an effect to...✅ Do this instead
Derive state from props or other stateCompute it during render in the hook (const fullName = [first, last].join(' ')) — the effect version renders once with the stale value first
Reset state when a prop changesPass key={id} to the child so React remounts it with fresh state
Notify the parent that something changedCall the parent's callback in the event handler that caused the change
Subscribe to an external storeUse the store's own hook (useMyStore((s) => s.x)) or useSyncExternalStore
Run one-time setup (analytics, i18n, client config)Call it at module scope or in the app entry — a component effect runs again on every mount
Fetch server dataUse a TanStack Query hook (see the Extraction Triggers table)

What's left is a legitimate effect: an event listener, an interval, a widget instance, an imperative browser API — and each returns a cleanup. Per component-hook-separation.md, all of them live in the co-located hook, never in the component body.


Hardcoded Value Extraction (Principle)

Hardcoded strings (routes, endpoints, query keys) drift into bugs when copy-pasted. Extract them to a single source of truth so the type system catches typos.

This skill covers the why. For project-specific extraction locations and patterns (where links lives, how endpoint constants are structured, query key conventions), see your project's centralized-links.md and tanstack-query.md rules.


Extraction Triggers

When to Extract

TriggerAction
Used 3+ timesExtract to shared location
50+ lines in componentSplit into sub-components
Nested conditionalExtract to child component
useEffect with fetchExtract to custom hook
Data transformationMove to hook or service
Business rule checkMove to domain service

When NOT to Extract

SituationKeep Inline
Used onceDon't premature-abstract
Simple ternaryNot every condition needs extraction
Short componentDon't split for the sake of splitting
Prototype codeWait until patterns emerge
Thin wrapperA div with className + children is not worth a component

Inline vs Extract: The Wrapper Test

A component is a thin wrapper if it:

  • Has < 10 lines of JSX
  • Has no hook or logic (just props → JSX)
  • Adds only styling/layout (a div with className)
  • Is used in exactly one place

Thin wrappers should be inlined. They add indirection without hiding complexity.

// ❌ Thin wrapper — just a styled div
export const PageToolbar = ({ children, className }) => (
  <div className={cn('flex items-center gap-2 border-b px-4 py-2', className)}>
    {children}
  </div>
)

// ✅ Inline it in the parent
<div className="flex items-center gap-2 border-b border-border px-4 py-2">
  <BackButton href={backHref} />
  <CheckoutGuideDialog ... />
</div>

Extract when: the component has its own hook, manages state, or is used in 2+ places.


Anti-Patterns

See references/anti-patterns.md for detailed examples including:

  • Component Knows Too Much -- mixing state, fetching, transforms, and business logic
  • Shallow Abstraction -- wrappers that add no value vs deep modules
  • Validation in Wrong Place -- scattered checks vs single boundary validation
  • Domain Re-Exporting Wire Types -- z.infer of a DTO schema passed off as the domain model

Mapping to create-feature Structure

See references/layer-mapping.md for the detailed diagram mapping these 3 layers to the create-feature folder structure.


Next Step

Understand Principles (this skill)
        ↓
Scaffold Structure (create-feature skill)
        ↓
Write Tests (frontend-testing skill)

Source

Adapted from Alex Kondov's "Clean Architecture in React" from The Full-Stack Tao.

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.