| name | frontend-philosophy |
| description | Frontend design philosophy and coding conventions for React/Preact applications. Enforces DHH-inspired simplicity, 8-section component structure, TanStack Query server state management, and strict anti-patterns (no useCallback, no useEffect for state sync, no barrel files, no TypeScript). Use when: (1) scaffolding new frontend projects, (2) building React/Preact components, (3) implementing features with state management, (4) reviewing or refactoring frontend code, (5) setting up project structure and file organization. Triggers on: "scaffold", "new project", "build a component", "create a page", "add a feature", "frontend", "React", "Preact", or any frontend implementation task.
|
Frontend Philosophy
Authoritative conventions for all React/Preact work. These are not suggestions ā they are the standard. When in doubt, choose the simpler path.
"Every line of code is a liability. The best code is no code. The second best is simple, boring code that obviously works."
Core Principles
- Simplicity over cleverness ā Write code a junior dev understands
- Convention over configuration ā Follow existing patterns, don't invent
- Optimize for understanding ā Code is read 10x more than written
- Embrace the monolith ā Colocate related concerns, don't split prematurely
- Performance through simplicity ā Fastest code is code that doesn't run
Stack Defaults
| Purpose | Default | Notes |
|---|
| Runtime/pkg | Bun | Fallback: npm. Never pnpm/yarn |
| Bundler | Vite | With @/ path alias |
| Framework | React or Preact | + TanStack Query/Router |
| Styling | Tailwind CSS | Utility-first, cn() for merging |
| UI primitives | shadcn/ui pattern | CVA for variants, Radix primitives |
| Server state | TanStack Query | ONLY source of truth for API data |
| Client state | Zustand or Preact Signals | UI state only (modals, theme, filters) |
| Icons | @remixicon/react | Ri*Line for outline, Ri*Fill for solid |
| Types | No TypeScript | .jsx components, .js utilities |
Why No TypeScript
Speed over ceremony. Type gymnastics add grief, not joy. Runtime validation at system boundaries, shared constants, JSDoc for complex functions.
File Conventions
src/
āāā components/
ā āāā ui/ # Base UI (shadcn pattern)
ā āāā billing/ # Feature-based grouping
ā āāā support/ # NOT type-based (lists/, forms/, displays/)
ā āāā common/ # Shared across features
āāā hooks/ # Custom hooks (use* prefix)
āāā stores/ # Zustand stores
āāā constants/ # Shared constants
āāā lib/ # Utilities (cn, formatters)
āāā pages/ # Route-level components
āāā api/ # API client
Naming rules:
.jsx components, .js utilities ā always
- PascalCase component files, camelCase everything else
handle* event handlers, use* hooks, render* render methods
- Feature-based folders, never type-based
- No barrel files (index.ts/js re-exports) ā ever
- No comments unless logic is truly non-obvious
Component Structure (8 Sections)
Every component follows this order. See references/component-patterns.md for full examples.
const Component = ({ prop1, prop2 }) => {
const { data, isLoading, error } = useQuery({ ... });
const { theme } = useThemeStore();
const [isOpen, setIsOpen] = useState(false);
const formRef = useRef(null);
const { isOnline } = useNetworkStatus();
const isValid = email.includes("@");
const fullName = `${data?.first} ${data?.last}`;
useEffect(() => { ... }, [deps]);
const handleSubmit = () => { ... };
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return ( ... );
};
State Management Rules
Server State = TanStack Query ONLY
const { data: users } = useQuery({
queryKey: ["users", filters],
queryFn: () => api.get("/users", { params: filters }),
staleTime: 5 * 60 * 1000,
gcTime: 60 * 60 * 1000,
});
const [users, setUsers] = useState([]);
useEffect(() => { fetchUsers().then(setUsers); }, []);
Client State = Zustand or Signals
Only for UI concerns: modals, theme, sidebar, filters, form inputs.
Derive, Don't Duplicate
const hasError = error !== null;
const activeUsers = users?.filter(u => u.isActive);
const [hasError, setHasError] = useState(false);
useEffect(() => setHasError(error !== null), [error]);
Data Fetching Hierarchy
- Pages fetch all required data
- Pass data as props to children
- Never refetch in children when parent has the data
Critical Anti-Patterns
These are hard rules, not guidelines.
| NEVER do this | Do this instead |
|---|
useEffect for state sync | Move logic to event handlers |
useCallback | Just define the function (almost never needed) |
useState for derived values | Compute in render |
| Duplicate server data in useState | TanStack Query is the source of truth |
| Barrel files (index.ts re-exports) | Import directly from the file |
useMemo without profiling first | Just compute it |
| Prop drilling beyond 2 levels | Composition or Zustand |
| Business logic in components | Extract to hooks or utilities |
| Multiple state management libs | TanStack Query + ONE client state lib |
| Inline styles or global CSS | Tailwind utilities |
| Premature abstraction | Rule of 3 ā wait for 3+ use cases |
| Custom wrappers around libraries | Use the library directly |
| Over-commenting | Self-documenting names |
Valid useEffect Uses
Only three:
- DOM synchronization (focus, scroll, resize listeners)
- External subscriptions (WebSocket, event emitters)
- Analytics/logging (page views, tracking)
Everything else belongs in an event handler or TanStack Query.
Formatting
Prettier defaults:
- 2-space indent, double quotes, semicolons
- 100 char print width, trailing commas ES5
- Tailwind plugin for class ordering
UI Patterns
- Buttons: always
rounded-full
- Cards: always
rounded-xl
- Use
cn() from @/lib/utils for class merging
- Use CVA (class-variance-authority) for component variants
- Composition over configuration ā build with children and slots
data-slot attributes for CSS targeting
- Early returns for loading/error states before main render
Code Review Questions
Before shipping, ask:
- Can a junior dev understand this?
- Is this the simplest solution that works?
- Does this follow existing patterns in the codebase?