There is no official React file structure. That's a feature. The cost is that you have to choose — and the wrong choice rots fast.
The two organizing axes
Every React project picks (consciously or not) along two axes:
- By type vs by feature. Type-first puts all hooks in
hooks/, all components incomponents/, all utilities inlib/. Feature-first puts a feature's hooks, components, and utilities together infeatures/chat/,features/auth/, etc. - Flat vs deep. Flat is one or two levels of nesting; deep can go five or six.
Type-first wins early (easier to find things by category). Feature-first wins later (easier to delete or extract a whole feature). The transition point is somewhere around 50 components.
The cwkPippa frontend layout
cwkPippa's frontend is feature-first, two levels deep:
src/
App.tsx # state lifting + route mounting
main.tsx # entry point
index.css # Tailwind v4 + theme tokens
components/
chat/ # InputArea, MessageList, MessageItem
sidebar/ # ConversationList, FolderTree
council/ # Council UI surface
admin/ # admin panels
settings/ # settings UI
hooks/ # useChat, useConversations, useHeartbeat
lib/ # api.ts, formatters, constants
types/ # shared TS types
Notice: components are grouped by feature (chat/, sidebar/, council/), but hooks and types stay type-first because they're shared. That hybrid is normal — pick the right axis per directory.
The state-lifting question
App.tsx in cwkPippa is the state-lifting root: top-level state lives there, child components receive props and callbacks. Some projects push state into Zustand stores or Context providers instead. There's no correct answer — but consistency matters. Pick one and stick.
Path aliases
Long relative imports (../../../components/chat/MessageList) are a smell. Vite supports path aliases via tsconfig.json's paths field. Set up @/* to point at src/* and your imports become @/components/chat/MessageList — readable and refactorable.