Home
React
5 Lessons for Structuring React Projects
Daniel Nguyen
Daniel Nguyen
August 06, 2025
2 min

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.

1. Organize by Feature, Not by File Type

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.

2. Create a shared/ Folder for Reusable Code

Some components and utilities are genuinely useful across multiple features.

src/
shared/
components/
Button/
Modal/
Input/
hooks/
useDebounce.ts
utils/
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.

3. Use Consistent Naming Conventions

Consistent naming makes a project easier to understand, especially for new contributors.

  • Components: PascalCase — UserProfile.tsx
  • Hooks: camelCase with the use prefix — useAuth.ts
  • Utilities: camelCase — formatDate.ts
  • Types: descriptive names — UserProfile.ts
  • Barrel files: use index.ts when it provides a clear public API.

For example:

// features/auth/hooks/useAuth.ts
export 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.

4. Separate Responsibilities into Layers

A maintainable React application separates UI, state management, data access, and infrastructure concerns.

LayerResponsibility
UIRender components and handle user interactions
StateManage client-side application state
DataFetch, cache, and update server data
InfrastructureHTTP clients, storage, logging, and configuration
UtilitiesReusable 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.

5. Start Simple and Scale Your Structure Gradually

A scalable project structure might look like this:

src/
app/
providers/
routes/
store/
features/
auth/
api/
model/
ui/
hooks/
index.ts
users/
api/
model/
ui/
index.ts
shared/
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.ts
import { 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.

Final Thoughts

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:

  1. Organize by feature.
  2. Share code intentionally.
  3. Keep naming consistent.
  4. Separate responsibilities.
  5. Scale the architecture with the application.

The best structure is the one that helps your team move quickly without making future changes unnecessarily difficult.


Tags

#React

Share

Daniel Nguyen

Daniel Nguyen

Frontend Developer

Frontend developer specializing in React, Next.js, and JavaScript. Writing practical guides on modern web development at Dev98.

Expertise

React
Next.js
JavaScript
TypeScript
Python

Social Media

githublinkedinyoutubewebsite

Related Posts

Micro Frontends
Micro Frontends with Vite and Module Federation
August 08, 2025
1 min
Dev98

Dev98

React · Next.js · Web development