A good project structure isn’t about creating more folders. It’s about making code easier to find, change, test, and maintain as an application grows.
Here are five principles that have helped me build more scalable React applications.
A common approach is to group files by type:
src/components/hooks/pages/utils/
This works for small applications, but as the project grows, related files become scattered across multiple folders.
Instead, organize around business features:
src/features/auth/api/hooks/ui/dashboard/api/hooks/ui/
Now, most changes stay within the relevant feature, making the codebase easier to navigate and maintain.
Rule: Keep code that changes together close together.
shared/ Folder for Reusable CodeSome components and utilities are genuinely useful across multiple features.
src/shared/components/Button/Modal/Input/hooks/useDebounce.tsutils/formatCurrency.ts
The key is to share code when reuse is real, not just because two components look similar.
Avoid turning shared/ into a dumping ground for everything that doesn’t fit elsewhere.
Rule: Share stable, reusable abstractions; keep feature-specific code inside its feature.
Consistent naming makes a project easier to understand, especially for new contributors.
UserProfile.tsxuse prefix — useAuth.tsformatDate.tsUserProfile.tsindex.ts when it provides a clear public API.For example:
// features/auth/hooks/useAuth.tsexport function useAuth() {// Authentication-related logic}
Avoid creating an index.ts in every folder without a clear purpose. Explicit exports can make dependencies easier to trace.
Rule: Consistency matters more than any particular naming convention.
A maintainable React application separates UI, state management, data access, and infrastructure concerns.
| Layer | Responsibility |
|---|---|
| UI | Render components and handle user interactions |
| State | Manage client-side application state |
| Data | Fetch, cache, and update server data |
| Infrastructure | HTTP clients, storage, logging, and configuration |
| Utilities | Reusable functions with focused responsibilities |
For example, React Query can manage server state, while Redux or Zustand can manage client-side state when needed.
function UserProfile({ userId }: { userId: string }) {const { data: user, isPending } = useUser(userId);if (isPending) return <p>Loading...</p>;if (!user) return <p>User not found.</p>;return <h1>{user.name}</h1>;}
Here, the component focuses on rendering, while useUser encapsulates the data-fetching logic.
Don’t over-engineer the boundaries, though. A small application doesn’t need a separate layer for every responsibility.
Rule: Separate concerns where it improves clarity, testing, or changeability.
A scalable project structure might look like this:
src/app/providers/routes/store/features/auth/api/model/ui/hooks/index.tsusers/api/model/ui/index.tsshared/components/hooks/utils/types/services/http/storage/config/tests/
app/ contains application setup, routing, and global providers.features/ contains business-specific functionality.shared/ contains reusable UI components and utilities.services/ contains infrastructure integrations.config/ contains environment parsing and application configuration.tests/ contains shared test setup and testing utilities.This is a starting point, not a universal template. For example, entities/ can be useful in a domain-heavy application, but unnecessary in a small dashboard.
For Vite, aliases can make imports easier to read:
// vite.config.tsimport { fileURLToPath, URL } from "node:url";import { defineConfig } from "vite";export default defineConfig({resolve: {alias: {"@app": fileURLToPath(new URL("./src/app", import.meta.url)),"@features": fileURLToPath(new URL("./src/features", import.meta.url)),"@shared": fileURLToPath(new URL("./src/shared", import.meta.url)),},},});
Configure matching paths in tsconfig.json as well so TypeScript understands the aliases.
Rule: Introduce architectural complexity only when the application needs it.
A good React architecture isn’t defined by how many folders you have. It’s defined by how easily developers can understand and change the system.
Remember these five principles:
The best structure is the one that helps your team move quickly without making future changes unnecessarily difficult.