frontend-architecture
A portable, framework-agnostic architecture style for any React or React Native frontend. Organizes apps into feature modules with page/screen directories, a strict server-state vs UI-state split, barrel-only cross-module imports, co-located styles, and clear component-promotion
Install
npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/frontend-architecture
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart
git clone https://github.com/sickn33/agentic-awesome-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole sickn33/agentic-awesome-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Frontend Architecture (portable, module-based)
When to Use
Use this skill when you need a portable, framework-agnostic architecture style for any React or React Native frontend. Organizes apps into feature modules with page/screen directories, a strict server-state vs UI-state split, barrel-only cross-module imports, co-located styles, and clear component-promotion rules....
Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others. This skill describes a structure and a set of rules, not a component library, a state library, or a visual style. It is deliberately global: the same module/page/state model maps onto Next.js (App Router), React + Vite (SPA), Remix, and Expo / React Native, and it works with any state-management and styling stack.
The goal: a codebase where any contributor can instantly answer three questions — "where does this code live?", "what is allowed to import what?", and "is this server state or UI state?" — without asking anyone. The structure makes the answers obvious.
0. The five core ideas
- Feature modules own their world. Each feature is a self-contained
modules/{feature}/folder with its own pages, components, hooks, state, types, and a single public barrel. - Pages/screens are directories, not files. A route is a folder that co-locates its component, its styles, and the components/hooks used only by it.
- State is split by origin. Server data lives in a query/cache layer. UI/client state lives in a store. They never overlap — regardless of which libraries you pick.
- Imports cross boundaries only through barrels. Reaching into another module's internals is forbidden; you import from
@/modules/{feature}and nothing deeper. - Code is promoted, not pre-placed. It starts as local as possible and moves outward only when a second consumer appears.
Everything below is the mechanical application of these five ideas. None of it is tied to a specific library — pick your stack in Sections 4 and 6.
1. Directory layout
The shape is identical across frameworks; only the routing layer on top differs (see Section 7).
src/
├── app/ or routes/ or navigation/ ← framework routing layer (thin — see §7)
├── modules/ ← feature modules (the heart of the app)
│ └── {feature}/
│ ├── index.ts ← PUBLIC BARREL — the only cross-module entry point
│ ├── README.md ← what this module owns, its routes, its data deps
│ ├── components/ ← components reused by 2+ pages IN THIS MODULE
│ ├── pages/ ← page/screen directories (one per route)
│ │ └── {page}/
│ │ ├── {page}.tsx ← the page/screen component
│ │ ├── {page}.styles.ts ← ALL styling for this page
│ │ ├── index.ts ← re-exports the page component
│ │ ├── components/ ← components used ONLY by this page
│ │ ├── hooks/ ← hooks used ONLY by this page
│ │ ├── constants/
│ │ └── README.md ← route, params, permissions, data deps
│ ├── hooks/ ← data hooks (query/mutation) + module hooks
│ ├── stores/ ← UI/client state store(s) — never server data
│ ├── services/ ← data-access (API calls) for this feature
│ ├── utils/ ← pure module utilities (co-located *.test.ts)
│ ├── constants/
│ └── types/ ← module request/response + view-model types
└── shared/ ← cross-module building blocks
├── components/ ← components used by 2+ MODULES
├── hooks/ ← cross-cutting hooks
├── api-client/ ← one typed client; the only place that talks to the network
├── store/ ← root store wiring (if your state lib needs one — see §4)
├── utils/ ← formatters, cn()/clsx, helpers
├── constants/
└── types/
Every folder that can be empty at scaffold time keeps a .gitkeep so the structure is visible from day one.
2. Feature modules
A module is a vertical slice of the product (e.g. auth, billing, dashboard, settings). It contains everything that feature needs and exposes a deliberately small surface.
2.1 The barrel (index.ts) is the contract
modules/{feature}/index.ts is the only thing other modules and the routing layer may import from. It re-exports:
- Page/screen components the router mounts.
- Data hooks other features legitimately need.
- The store hook/slice and its public types.
- Shared constants / types other features depend on.
// CORRECT — consume the public surface
import { InvoiceListPage, useInvoiceList } from "@/modules/invoice";
// WRONG — reaching into internals couples you to private structure
import { InvoiceListPage } from "@/modules/invoice/pages/invoice-list/invoice-list";
Keep the barrel curated. If something isn't exported, it's private by design. Group exports with short comments (pages, hooks, store, types) — future readers use the barrel as the module's API docs.
2.2 One module = one bounded context
Don't create utils modules or components modules. Modules map to product capabilities, not to technical layers. Technical building blocks live in shared/.
2.3 Module README
Each module's README.md states: what it owns, which routes render its pages, its data dependencies (which endpoints/hooks), and any cross-module rules. This is the first thing a new contributor reads.
3. Pages/screens as directories
A page is a route the router mounts (a "screen" in React Native). It is always a folder, never a loose file — even when it starts as a single component. This keeps growth in place: when the page needs a sub-component or a hook, there is already a home for it.
pages/{page}/
├── {page}.tsx ← the page/screen component
├── {page}.styles.ts ← every style for this page (no inline styles — see §5)
├── index.ts ← export { PageComponent } from "./{page}"
├── components/ ← used ONLY by this page
├── hooks/ ← used ONLY by this page
├── constants/
└── README.md ← route, params, permissions, data deps
The page README is short and high-signal: route path, expected params, required permissions/auth, and the hooks it depends on. It is the contract between the page and the rest of the app.
Why folders from the start: a page that begins as one file inevitably grows a sub-row component, a derived-totals hook, a styles file. If the page is a file, those land in arbitrary places. If the page is a folder, they have an obvious home and the diff stays readable.
4. State: split by origin (non-negotiable, library-agnostic)
Two kinds of state, two homes. Mixing them is the most common architectural failure this skill exists to prevent. The split is mandatory; the libraries are your choice.
| State kind | Examples | Lives in |
|---|---|---|
| Server state | fetched entities, lists, aggregates — anything the API owns | a query/cache layer (e.g. TanStack Query, RTK Query, SWR, Apollo) |
| UI / client state | open dialogs, table filters/sort, wizard step, draft being typed, preview toggles | a client store (e.g. Zustand, Redux Toolkit, MobX, Jotai, Valtio, or React Context) |
4.1 Hard rules (independent of library)
- Never mirror server responses into the client store. No copying fetched entities into Zustand/Redux/MobX. The query/cache layer is the single source of truth for server data.
- Never fetch inside components. Components read server data from a data hook and UI state from a store selector. They don't call the network client directly.
- Never drive continuous values through re-render state. Scroll progress, pointer position, drag offset — use refs / animation values, not render state (it re-renders the tree every frame).
- One store boundary per module. Whatever library you use, give each module one cohesive store unit (a Zustand hook, a Redux slice, a MobX class, a Jotai atom group) accessed via the module barrel. Components subscribe to the smallest slice they need to avoid needless re-renders.
4.2 Choosing a client-state library — same shape, different syntax
Pick one per project and stay consistent. Each maps onto "one store unit per module" cleanly. Note the I interface-naming convention: state interfaces are prefixed with I (e.g. IFeatureUiState).
Zustand — modules/{feature}/stores/{feature}.store.ts
import { create } from "zustand";
export interface IFeatureUiState {
isPreviewOpen: boolean;
filter: string;
togglePreview: () => void;
setFilter: (filter: string) => void;
reset: () => void;
}
const INITIAL_STATE = { isPreviewOpen: false, filter: "" } as const;
export const useFeatureUiStore = create<IFeatureUiState>()((set) => ({
...INITIAL_STATE,
togglePreview: () => set((s) => ({ isPreviewOpen: !s.isPreviewOpen })),
setFilter: (filter) => set({ filter }),
reset: () => set({ ...INITIAL_STATE }),
}));
Redux Toolkit — modules/{feature}/stores/{feature}.slice.ts (registered in shared/store/)
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
export interface IFeatureUiState {
isPreviewOpen: boolean;
filter: string;
}
const initialState: IFeatureUiState = { isPreviewOpen: false, filter: "" };
export const featureUiSlice = createSlice({
name: "featureUi",
initialState,
reducers: {
togglePreview: (s) => {
s.isPreviewOpen = !s.isPreviewOpen;
},
setFilter: (s, action: PayloadAction<string>) => {
s.filter = action.payload;
},
reset: () => initialState,
},
});
MobX — modules/{feature}/stores/{feature}.store.ts
import { makeAutoObservable } from "mobx";
export interface IFeatureUiState {
isPreviewOpen: boolean;
filter: string;
}
export class FeatureUiStore implements IFeatureUiState {
isPreviewOpen = false;
filter = "";
constructor() {
makeAutoObservable(this);
}
togglePreview = () => {
this.isPreviewOpen = !this.isPreviewOpen;
};
setFilter = (filter: string) => {
this.filter = filter;
};
reset = () => {
this.isPreviewOpen = false;
this.filter = "";
};
}
Jotai — modules/{feature}/stores/{feature}.atoms.ts
import { atom } from "jotai";
export const isPreviewOpenAtom = atom(false);
export const filterAtom = atom("");
Whichever you choose, keep the rules in §4.1 constant. The skill cares that server and UI state are separated and that each module owns one store unit — not which library draws the box.
4.3 Data layer (server state)
All network access goes through one typed client in shared/api-client/. Modules wrap it in query/mutation hooks and a key factory so caches and invalidation stay consistent.
// modules/invoice/hooks/invoiceKeys.ts — hierarchical key factory (TanStack Query style)
export const invoiceKeys = {
all: ["invoices"] as const,
lists: () => [...invoiceKeys.all, "list"] as const,
list: (params: IListParams) => [...invoiceKeys.lists(), params] as const,
details: () => [...invoiceKeys.all, "detail"] as const,
detail: (id: string) => [...invoiceKeys.details(), id] as const,
} as const;
Invalidating lists() refreshes every filtered page; detail(id) targets one entity. (RTK Query/SWR/Apollo express the same idea with tags/keys.) Components never write raw fetch() — they call useInvoiceList() / useCreateInvoice().
5. Styling: co-located, no inline styles (styling-library agnostic)
Keep styling out of JSX and out of the component body. Each page or component has a co-located styles file. The rule is constant; the syntax follows your styling stack.
- Tailwind (web):
{name}.styles.tsexports named class strings composed withcn()(clsx + tailwind-merge); variants viacva. JSX referencesstyles.header. - CSS Modules / vanilla-extract: a co-located
{name}.module.css/{name}.css.ts; JSX referencesstyles.header. - styled-components / Emotion: a co-located
{name}.styles.tsexporting styled components. - Tamagui (web + native): a co-located
{name}.styles.tsexportingstyled(...)components or acreateStyledContext/useStyletoken set; reference Tamagui tokens ($background,$space.4) — never hardcoded values inline. Tamagui is the recommended choice when you target both web and React Native from one codebase. - React Native StyleSheet / Nativewind: a co-located
{name}.styles.tsexportingStyleSheet.create({...})(or Nativewind classnames). JSX referencesstyles.header.
// invoice-list.styles.ts (Tailwind example)
export const invoiceListStyles = {
page: "flex flex-col gap-8",
header: "flex flex-col gap-1.5",
title: "text-3xl font-semibold tracking-tight",
} as const;
// invoice-list.styles.ts (Tamagui example — works on web AND native)
import { styled, YStack, Text } from "tamagui";
export const InvoiceListPage = styled(YStack, { flex: 1, gap: "$8" });
export const InvoiceListHeader = styled(YStack, { gap: "$1.5" });
export const InvoiceListTitle = styled(Text, {
fontSize: "$8",
fontWeight: "600",
});
No inline style={{...}} literals in the component body, on any stack. Why: styling drifts and duplicates when it lives inline. A co-located styles file gives one place to audit spacing rhythm, theme correctness, and responsive behavior per surface. Document non-obvious choices (accent locks, breakpoints) in comments there.
This skill does not dictate the visual design — pair it with a design/component skill for that. It dictates only where styling lives.
6. Naming conventions
Consistent naming makes the structure self-describing.
- Interfaces are prefixed with
I—IFeatureUiState,IInvoiceListParams,IUserProfile. Type aliases (unions, mapped types, primitives) are not prefixed (type SortDirection = "asc" | "desc"). - Components:
PascalCasefiles and exports —InvoiceListPage.tsx,LineItemRow.tsx. - Pages/screens:
kebab-casedirectories, the component file matches —pages/invoice-list/invoice-list.tsx. - Hooks:
useCamelCase—useInvoiceList,useFeatureUiStore. - Stores:
{feature}.store.ts(Zustand/MobX),{feature}.slice.ts(Redux),{feature}.atoms.ts(Jotai). Hook isuse{Feature}{Purpose}Store. - Styles:
{name}.styles.tsco-located with its owner. - Constants:
SCREAMING_SNAKE_CASEvalues;kebab-caseorcamelCasefiles. - Barrels: always
index.ts.
7. Framework adapters
The module/page/state model is constant. Only the thin routing layer on top changes. Pages always live in modules/; the routing layer just mounts them.
7.1 Next.js (App Router)
src/app/holds route segments and route groups ((marketing),(app),(public)) for layout/auth boundaries. Route files are thin: import a page component from a module barrel and render it.- Default to Server Components; mark interactive leaves
"use client". Providers (query client, store, theme) live in a"use client"boundary.
// app/(app)/invoices/page.tsx — thin route file
import { InvoiceListPage } from "@/modules/invoice";
export default function Page() {
return <InvoiceListPage />;
}
7.2 React + Vite (SPA)
- A
src/routes/(or singlerouter.tsx) declares the route table (React Router / TanStack Router) and maps paths to module page components. Everything is client-side. Wrap the tree once with the query-client and store/theme providers at the app root.
7.3 Remix
- Route modules in
app/routes/stay thin and re-export/mount module page components; loaders/actions delegate to the module'sservices/. Module boundaries are unchanged.
7.4 Expo / React Native
- Routing is Expo Router (file-based, in
app/) or React Navigation (navigation/). Route/screen files are thin and import screen components from module barrels. - "Pages" are "screens" — same directory pattern:
pages/{screen}/{screen}.tsx+{screen}.styles.ts. - Query layer + client store run unchanged (TanStack Query, Zustand, Redux, MobX, Jotai all work in RN). The typed
api-clientis shared logic and works as-is. - Styling uses Tamagui (recommended for shared web+native),
StyleSheet, or Nativewind. Keep module logic DOM-free.
// app/invoices/index.tsx (Expo Router) — thin screen file
import { InvoiceListScreen } from "@/modules/invoice";
export default InvoiceListScreen;
7.5 Sharing across web + native
If you target both web and Expo, push framework-free code (types, validators, formatters, the API client contract) into a shared package consumed by both apps, and prefer Tamagui for components that must render on both. Module boundaries stay the same on both sides.
8. Conventions checklist (enforce in review)
- New feature → new
modules/{feature}/withindex.ts+README.md, not files scattered intoshared/. - New route → a page/screen directory (
{page}.tsx+{page}.styles.ts+index.ts+README.md), not a loose file. - Cross-module imports go through the barrel (
@/modules/{feature}) — no deep internal paths. - Server data is in the query/cache layer; UI state is in the module store; neither leaks into the other (whatever libraries are chosen).
- No
fetch()in components — only typed data hooks built on the shared client. - No inline styles — co-located
{name}.styles.ts(Tailwind/CSS Modules/Tamagui/StyleSheet/styled-components). - Components/hooks/utils placed at the narrowest scope; promoted only when a 2nd consumer appears.
- One store unit per module, accessed via the barrel, with selectors and a
reset. - Interfaces use the
Iprefix; components/hooks/files follow §6. - Query keys/tags come from a per-module factory; invalidation is hierarchical.
- Routing files are thin — they mount module pages and own only layout/auth boundaries.
- Module/page READMEs updated when routes, params, or data deps change.
9. Component promotion (start local, move outward)
A component is born in the narrowest scope that uses it and is promoted only when a second consumer appears. Never pre-place a component "because it might be reused."
| A component used by… | Lives in | Imported as |
|---|---|---|
| Only one page | pages/{page}/components/ |
relative path within the page |
| 2+ pages in one module | modules/{feature}/components/ |
@/modules/{feature} (via barrel) |
| 2+ modules | shared/components/ |
@/shared/... |
| 2+ apps / repos | a published design-system package | the package name |
The same ladder applies to hooks, utils, and constants: local → module → shared → package. Promotion is a deliberate move (update the import sites), not a guess made up front.
10. How to apply this skill
Scaffolding a new app: create src/modules/, src/shared/, and the framework routing layer (§7). Add the shared api-client, the query layer, and your chosen client-store provider. Drop a store template into the first module.
Adding a feature: create modules/{feature}/ with the full subfolder set (pages/ components/ hooks/ stores/ services/ utils/ constants/ types/), a curated index.ts, and a README.md. Build the first screen as a page directory.
Deciding where code goes: ask "who consumes this?" → narrowest scope wins (§9). Ask "where did this data come from?" → server = query layer, UI = store (§4).
Reviewing structure: run the checklist in §8. The most valuable catches are state-origin leaks (server data in the client store) and deep cross-module imports (bypassing the barrel) — both erode the architecture fastest.
Publishing / installing this skill
This skill follows the Anthropic SKILL.md format and is portable across agents. To make it installable and discoverable (e.g. on skills.sh / npx skills):
- Put this folder under a
skills/directory in a public GitHub repo (path likeskills/frontend-architecture/SKILL.md). - Keep the frontmatter
nameand a high-signaldescription(above) — that description is what discovery indexes match against. - Install from any project with:
npx skills add <org>/<repo> --skill "frontend-architecture". - Non-
SKILL.mdagents can be pointed here fromAGENTS.md/CLAUDE.md; Kiro can mirror it as a steering file.
Limitations
- Use this skill only when the task clearly matches its upstream source and local project context.
- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.
Files (agentic-awesome-skills)
-
SKILL.md 22.3 KB
--- name: frontend-architecture description: A portable, framework-agnostic architecture style for any React or React Native frontend. Organizes apps into feature modules with page/screen directories, a strict server-state vs UI-state split, barrel-only cross-module imports, co-located styles, and clear component-promotion rules.... risk: critical source: https://github.com/stareezy-1/frontend-architecture-skill/tree/main/skills/frontend-architecture source_repo: stareezy-1/frontend-architecture-skill source_type: community date_added: 2026-07-01 license: MIT license_source: https://github.com/stareezy-1/frontend-architecture-skill/blob/main/LICENSE --- # Frontend Architecture (portable, module-based) ## When to Use Use this skill when you need a portable, framework-agnostic architecture style for any React or React Native frontend. Organizes apps into feature modules with page/screen directories, a strict server-state vs UI-state split, barrel-only cross-module imports, co-located styles, and clear component-promotion rules.... > Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others. > This skill describes a **structure and a set of rules**, not a component library, a state library, or a visual style. > It is deliberately global: the same module/page/state model maps onto > **Next.js (App Router)**, **React + Vite (SPA)**, **Remix**, and **Expo / React Native**, and it works > with **any** state-management and styling stack. The goal: a codebase where any contributor can instantly answer three questions — **"where does this code live?"**, **"what is allowed to import what?"**, and **"is this server state or UI state?"** — without asking anyone. The structure makes the answers obvious. --- ## 0. The five core ideas 1. **Feature modules own their world.** Each feature is a self-contained `modules/{feature}/` folder with its own pages, components, hooks, state, types, and a single public barrel. 2. **Pages/screens are directories, not files.** A route is a folder that co-locates its component, its styles, and the components/hooks used only by it. 3. **State is split by origin.** Server data lives in a query/cache layer. UI/client state lives in a store. They never overlap — regardless of which libraries you pick. 4. **Imports cross boundaries only through barrels.** Reaching into another module's internals is forbidden; you import from `@/modules/{feature}` and nothing deeper. 5. **Code is promoted, not pre-placed.** It starts as local as possible and moves outward only when a second consumer appears. Everything below is the mechanical application of these five ideas. None of it is tied to a specific library — pick your stack in Sections 4 and 6. --- ## 1. Directory layout The shape is identical across frameworks; only the routing layer on top differs (see Section 7). ``` src/ ├── app/ or routes/ or navigation/ ← framework routing layer (thin — see §7) ├── modules/ ← feature modules (the heart of the app) │ └── {feature}/ │ ├── index.ts ← PUBLIC BARREL — the only cross-module entry point │ ├── README.md ← what this module owns, its routes, its data deps │ ├── components/ ← components reused by 2+ pages IN THIS MODULE │ ├── pages/ ← page/screen directories (one per route) │ │ └── {page}/ │ │ ├── {page}.tsx ← the page/screen component │ │ ├── {page}.styles.ts ← ALL styling for this page │ │ ├── index.ts ← re-exports the page component │ │ ├── components/ ← components used ONLY by this page │ │ ├── hooks/ ← hooks used ONLY by this page │ │ ├── constants/ │ │ └── README.md ← route, params, permissions, data deps │ ├── hooks/ ← data hooks (query/mutation) + module hooks │ ├── stores/ ← UI/client state store(s) — never server data │ ├── services/ ← data-access (API calls) for this feature │ ├── utils/ ← pure module utilities (co-located *.test.ts) │ ├── constants/ │ └── types/ ← module request/response + view-model types └── shared/ ← cross-module building blocks ├── components/ ← components used by 2+ MODULES ├── hooks/ ← cross-cutting hooks ├── api-client/ ← one typed client; the only place that talks to the network ├── store/ ← root store wiring (if your state lib needs one — see §4) ├── utils/ ← formatters, cn()/clsx, helpers ├── constants/ └── types/ ``` Every folder that can be empty at scaffold time keeps a `.gitkeep` so the structure is visible from day one. --- ## 2. Feature modules A module is a vertical slice of the product (e.g. `auth`, `billing`, `dashboard`, `settings`). It contains everything that feature needs and exposes a deliberately small surface. ### 2.1 The barrel (`index.ts`) is the contract `modules/{feature}/index.ts` is the **only** thing other modules and the routing layer may import from. It re-exports: - Page/screen components the router mounts. - Data hooks other features legitimately need. - The store hook/slice and its public types. - Shared constants / types other features depend on. ```ts // CORRECT — consume the public surface import { InvoiceListPage, useInvoiceList } from "@/modules/invoice"; // WRONG — reaching into internals couples you to private structure import { InvoiceListPage } from "@/modules/invoice/pages/invoice-list/invoice-list"; ``` Keep the barrel curated. If something isn't exported, it's private by design. Group exports with short comments (pages, hooks, store, types) — future readers use the barrel as the module's API docs. ### 2.2 One module = one bounded context Don't create `utils` modules or `components` modules. Modules map to product capabilities, not to technical layers. Technical building blocks live in `shared/`. ### 2.3 Module README Each module's `README.md` states: what it owns, which routes render its pages, its data dependencies (which endpoints/hooks), and any cross-module rules. This is the first thing a new contributor reads. --- ## 3. Pages/screens as directories A page is a route the router mounts (a "screen" in React Native). It is **always a folder**, never a loose file — even when it starts as a single component. This keeps growth in place: when the page needs a sub-component or a hook, there is already a home for it. ``` pages/{page}/ ├── {page}.tsx ← the page/screen component ├── {page}.styles.ts ← every style for this page (no inline styles — see §5) ├── index.ts ← export { PageComponent } from "./{page}" ├── components/ ← used ONLY by this page ├── hooks/ ← used ONLY by this page ├── constants/ └── README.md ← route, params, permissions, data deps ``` The page README is short and high-signal: route path, expected params, required permissions/auth, and the hooks it depends on. It is the contract between the page and the rest of the app. **Why folders from the start:** a page that begins as one file inevitably grows a sub-row component, a derived-totals hook, a styles file. If the page is a file, those land in arbitrary places. If the page is a folder, they have an obvious home and the diff stays readable. --- ## 4. State: split by origin (non-negotiable, library-agnostic) Two kinds of state, two homes. Mixing them is the most common architectural failure this skill exists to prevent. **The split is mandatory; the libraries are your choice.** | State kind | Examples | Lives in | | --------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | **Server state** | fetched entities, lists, aggregates — anything the API owns | a **query/cache layer** (e.g. TanStack Query, RTK Query, SWR, Apollo) | | **UI / client state** | open dialogs, table filters/sort, wizard step, draft being typed, preview toggles | a **client store** (e.g. Zustand, Redux Toolkit, MobX, Jotai, Valtio, or React Context) | ### 4.1 Hard rules (independent of library) - **Never mirror server responses into the client store.** No copying fetched entities into Zustand/Redux/MobX. The query/cache layer is the single source of truth for server data. - **Never fetch inside components.** Components read server data from a data hook and UI state from a store selector. They don't call the network client directly. - **Never drive continuous values through re-render state.** Scroll progress, pointer position, drag offset — use refs / animation values, not render state (it re-renders the tree every frame). - **One store boundary per module.** Whatever library you use, give each module one cohesive store unit (a Zustand hook, a Redux slice, a MobX class, a Jotai atom group) accessed via the module barrel. Components subscribe to the smallest slice they need to avoid needless re-renders. ### 4.2 Choosing a client-state library — same shape, different syntax Pick one per project and stay consistent. Each maps onto "one store unit per module" cleanly. Note the **`I` interface-naming convention**: state interfaces are prefixed with `I` (e.g. `IFeatureUiState`). **Zustand** — `modules/{feature}/stores/{feature}.store.ts` ```ts import { create } from "zustand"; export interface IFeatureUiState { isPreviewOpen: boolean; filter: string; togglePreview: () => void; setFilter: (filter: string) => void; reset: () => void; } const INITIAL_STATE = { isPreviewOpen: false, filter: "" } as const; export const useFeatureUiStore = create<IFeatureUiState>()((set) => ({ ...INITIAL_STATE, togglePreview: () => set((s) => ({ isPreviewOpen: !s.isPreviewOpen })), setFilter: (filter) => set({ filter }), reset: () => set({ ...INITIAL_STATE }), })); ``` **Redux Toolkit** — `modules/{feature}/stores/{feature}.slice.ts` (registered in `shared/store/`) ```ts import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; export interface IFeatureUiState { isPreviewOpen: boolean; filter: string; } const initialState: IFeatureUiState = { isPreviewOpen: false, filter: "" }; export const featureUiSlice = createSlice({ name: "featureUi", initialState, reducers: { togglePreview: (s) => { s.isPreviewOpen = !s.isPreviewOpen; }, setFilter: (s, action: PayloadAction<string>) => { s.filter = action.payload; }, reset: () => initialState, }, }); ``` **MobX** — `modules/{feature}/stores/{feature}.store.ts` ```ts import { makeAutoObservable } from "mobx"; export interface IFeatureUiState { isPreviewOpen: boolean; filter: string; } export class FeatureUiStore implements IFeatureUiState { isPreviewOpen = false; filter = ""; constructor() { makeAutoObservable(this); } togglePreview = () => { this.isPreviewOpen = !this.isPreviewOpen; }; setFilter = (filter: string) => { this.filter = filter; }; reset = () => { this.isPreviewOpen = false; this.filter = ""; }; } ``` **Jotai** — `modules/{feature}/stores/{feature}.atoms.ts` ```ts import { atom } from "jotai"; export const isPreviewOpenAtom = atom(false); export const filterAtom = atom(""); ``` > Whichever you choose, keep the rules in §4.1 constant. The skill cares that server and UI state are separated and that each module owns one store unit — not which library draws the box. ### 4.3 Data layer (server state) All network access goes through **one typed client** in `shared/api-client/`. Modules wrap it in query/mutation hooks and a **key factory** so caches and invalidation stay consistent. ```ts // modules/invoice/hooks/invoiceKeys.ts — hierarchical key factory (TanStack Query style) export const invoiceKeys = { all: ["invoices"] as const, lists: () => [...invoiceKeys.all, "list"] as const, list: (params: IListParams) => [...invoiceKeys.lists(), params] as const, details: () => [...invoiceKeys.all, "detail"] as const, detail: (id: string) => [...invoiceKeys.details(), id] as const, } as const; ``` Invalidating `lists()` refreshes every filtered page; `detail(id)` targets one entity. (RTK Query/SWR/Apollo express the same idea with tags/keys.) Components never write raw `fetch()` — they call `useInvoiceList()` / `useCreateInvoice()`. --- ## 5. Styling: co-located, no inline styles (styling-library agnostic) Keep styling out of JSX and out of the component body. Each page or component has a **co-located styles file**. The rule is constant; the syntax follows your styling stack. - **Tailwind (web):** `{name}.styles.ts` exports named class strings composed with `cn()` (clsx + tailwind-merge); variants via `cva`. JSX references `styles.header`. - **CSS Modules / vanilla-extract:** a co-located `{name}.module.css` / `{name}.css.ts`; JSX references `styles.header`. - **styled-components / Emotion:** a co-located `{name}.styles.ts` exporting styled components. - **Tamagui (web + native):** a co-located `{name}.styles.ts` exporting `styled(...)` components or a `createStyledContext` / `useStyle` token set; reference Tamagui tokens (`$background`, `$space.4`) — never hardcoded values inline. Tamagui is the recommended choice when you target **both web and React Native** from one codebase. - **React Native StyleSheet / Nativewind:** a co-located `{name}.styles.ts` exporting `StyleSheet.create({...})` (or Nativewind classnames). JSX references `styles.header`. ```ts // invoice-list.styles.ts (Tailwind example) export const invoiceListStyles = { page: "flex flex-col gap-8", header: "flex flex-col gap-1.5", title: "text-3xl font-semibold tracking-tight", } as const; ``` ```ts // invoice-list.styles.ts (Tamagui example — works on web AND native) import { styled, YStack, Text } from "tamagui"; export const InvoiceListPage = styled(YStack, { flex: 1, gap: "$8" }); export const InvoiceListHeader = styled(YStack, { gap: "$1.5" }); export const InvoiceListTitle = styled(Text, { fontSize: "$8", fontWeight: "600", }); ``` **No inline `style={{...}}` literals in the component body**, on any stack. Why: styling drifts and duplicates when it lives inline. A co-located styles file gives one place to audit spacing rhythm, theme correctness, and responsive behavior per surface. Document non-obvious choices (accent locks, breakpoints) in comments there. This skill does not dictate the _visual_ design — pair it with a design/component skill for that. It dictates only _where styling lives_. --- ## 6. Naming conventions Consistent naming makes the structure self-describing. - **Interfaces are prefixed with `I`** — `IFeatureUiState`, `IInvoiceListParams`, `IUserProfile`. Type aliases (unions, mapped types, primitives) are **not** prefixed (`type SortDirection = "asc" | "desc"`). - **Components**: `PascalCase` files and exports — `InvoiceListPage.tsx`, `LineItemRow.tsx`. - **Pages/screens**: `kebab-case` directories, the component file matches — `pages/invoice-list/invoice-list.tsx`. - **Hooks**: `useCamelCase` — `useInvoiceList`, `useFeatureUiStore`. - **Stores**: `{feature}.store.ts` (Zustand/MobX), `{feature}.slice.ts` (Redux), `{feature}.atoms.ts` (Jotai). Hook is `use{Feature}{Purpose}Store`. - **Styles**: `{name}.styles.ts` co-located with its owner. - **Constants**: `SCREAMING_SNAKE_CASE` values; `kebab-case` or `camelCase` files. - **Barrels**: always `index.ts`. --- ## 7. Framework adapters The module/page/state model is constant. Only the thin routing layer on top changes. Pages always live in `modules/`; the routing layer just **mounts** them. ### 7.1 Next.js (App Router) - `src/app/` holds route segments and route groups (`(marketing)`, `(app)`, `(public)`) for layout/auth boundaries. Route files are thin: import a page component from a module barrel and render it. - Default to **Server Components**; mark interactive leaves `"use client"`. Providers (query client, store, theme) live in a `"use client"` boundary. ```tsx // app/(app)/invoices/page.tsx — thin route file import { InvoiceListPage } from "@/modules/invoice"; export default function Page() { return <InvoiceListPage />; } ``` ### 7.2 React + Vite (SPA) - A `src/routes/` (or single `router.tsx`) declares the route table (React Router / TanStack Router) and maps paths to module page components. Everything is client-side. Wrap the tree once with the query-client and store/theme providers at the app root. ### 7.3 Remix - Route modules in `app/routes/` stay thin and re-export/mount module page components; loaders/actions delegate to the module's `services/`. Module boundaries are unchanged. ### 7.4 Expo / React Native - Routing is **Expo Router** (file-based, in `app/`) or React Navigation (`navigation/`). Route/screen files are thin and import screen components from module barrels. - "Pages" are "screens" — same directory pattern: `pages/{screen}/{screen}.tsx` + `{screen}.styles.ts`. - Query layer + client store run unchanged (TanStack Query, Zustand, Redux, MobX, Jotai all work in RN). The typed `api-client` is shared logic and works as-is. - Styling uses **Tamagui** (recommended for shared web+native), `StyleSheet`, or Nativewind. Keep module logic DOM-free. ```tsx // app/invoices/index.tsx (Expo Router) — thin screen file import { InvoiceListScreen } from "@/modules/invoice"; export default InvoiceListScreen; ``` ### 7.5 Sharing across web + native If you target both web and Expo, push framework-free code (types, validators, formatters, the API client contract) into a shared package consumed by both apps, and prefer **Tamagui** for components that must render on both. Module boundaries stay the same on both sides. --- ## 8. Conventions checklist (enforce in review) - [ ] New feature → new `modules/{feature}/` with `index.ts` + `README.md`, not files scattered into `shared/`. - [ ] New route → a **page/screen directory** (`{page}.tsx` + `{page}.styles.ts` + `index.ts` + `README.md`), not a loose file. - [ ] Cross-module imports go through the barrel (`@/modules/{feature}`) — no deep internal paths. - [ ] Server data is in the query/cache layer; UI state is in the module store; **neither leaks into the other** (whatever libraries are chosen). - [ ] No `fetch()` in components — only typed data hooks built on the shared client. - [ ] No inline styles — co-located `{name}.styles.ts` (Tailwind/CSS Modules/Tamagui/StyleSheet/styled-components). - [ ] Components/hooks/utils placed at the narrowest scope; promoted only when a 2nd consumer appears. - [ ] One store unit per module, accessed via the barrel, with selectors and a `reset`. - [ ] Interfaces use the `I` prefix; components/hooks/files follow §6. - [ ] Query keys/tags come from a per-module factory; invalidation is hierarchical. - [ ] Routing files are thin — they mount module pages and own only layout/auth boundaries. - [ ] Module/page READMEs updated when routes, params, or data deps change. --- ## 9. Component promotion (start local, move outward) A component is born in the narrowest scope that uses it and is **promoted** only when a second consumer appears. Never pre-place a component "because it might be reused." | A component used by… | Lives in | Imported as | | ---------------------- | --------------------------------- | ---------------------------------- | | Only one page | `pages/{page}/components/` | relative path within the page | | 2+ pages in one module | `modules/{feature}/components/` | `@/modules/{feature}` (via barrel) | | 2+ modules | `shared/components/` | `@/shared/...` | | 2+ apps / repos | a published design-system package | the package name | The same ladder applies to **hooks**, **utils**, and **constants**: local → module → shared → package. Promotion is a deliberate move (update the import sites), not a guess made up front. --- ## 10. How to apply this skill **Scaffolding a new app:** create `src/modules/`, `src/shared/`, and the framework routing layer (§7). Add the shared `api-client`, the query layer, and your chosen client-store provider. Drop a store template into the first module. **Adding a feature:** create `modules/{feature}/` with the full subfolder set (`pages/ components/ hooks/ stores/ services/ utils/ constants/ types/`), a curated `index.ts`, and a `README.md`. Build the first screen as a page directory. **Deciding where code goes:** ask "who consumes this?" → narrowest scope wins (§9). Ask "where did this data come from?" → server = query layer, UI = store (§4). **Reviewing structure:** run the checklist in §8. The most valuable catches are state-origin leaks (server data in the client store) and deep cross-module imports (bypassing the barrel) — both erode the architecture fastest. --- ## Publishing / installing this skill This skill follows the Anthropic `SKILL.md` format and is portable across agents. To make it installable and discoverable (e.g. on skills.sh / `npx skills`): 1. Put this folder under a `skills/` directory in a **public GitHub repo** (path like `skills/frontend-architecture/SKILL.md`). 2. Keep the frontmatter `name` and a high-signal `description` (above) — that description is what discovery indexes match against. 3. Install from any project with: `npx skills add <org>/<repo> --skill "frontend-architecture"`. 4. Non-`SKILL.md` agents can be pointed here from `AGENTS.md` / `CLAUDE.md`; Kiro can mirror it as a steering file. ## Limitations - Use this skill only when the task clearly matches its upstream source and local project context. - Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes. - Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.