DEV Community

Cover image for Structuring Large Frontend Projects with React (Without Losing Your Mind)
Akshay Kurve
Akshay Kurve

Posted on

Structuring Large Frontend Projects with React (Without Losing Your Mind)

When you start a React project, everything feels simple.

A few components, a couple of folders, maybe some state — done.

But fast forward a few weeks (or months), and suddenly:

  • Files are everywhere
  • Components are tightly coupled
  • State is hard to manage
  • Adding a feature feels risky

This isn’t a React problem.

It’s a project structure problem.

In this article, we’ll break down how to structure large React applications in a way that scales — while keeping things simple enough for beginners to follow.


Table of Contents


Why Structure Matters

When you start a React project, everything feels simple. A few components, a couple of folders, some state -- done.

But fast forward a few months, and suddenly files are everywhere, components are tightly coupled, state is difficult to trace, and adding a feature feels risky. This is not a React problem. It is a project structure problem.

A good structure helps you:

  • Find files faster
  • Avoid bugs caused by tangled dependencies
  • Scale your application without chaos
  • Onboard new developers with minimal friction

A bad structure leads to:

  • Constant "Where is this file?" moments
  • Duplicated logic across folders
  • Tight coupling between unrelated components
  • Fear of making changes

Think of your project structure like a city layout. If roads are random, traffic becomes a nightmare. The same applies to your codebase.


The Biggest Mistake Beginners Make

Most beginners organize React applications like this:

src/
  components/
  pages/
  hooks/
  utils/
Enter fullscreen mode Exit fullscreen mode

At first glance, this looks clean. But as your application grows, components/ becomes a dumping ground, related files scatter across folders, and you find yourself jumping between directories to understand a single feature.

This is called type-based structure, and it does not scale well. As one widely-referenced guide puts it, there will eventually be "too many components in your components/ folder."


The Better Approach: Feature-Based Structure

Instead of grouping by file type, group by feature.

Example

src/
  features/
    auth/
      components/
      hooks/
      services/
      authSlice.ts
    dashboard/
      components/
      hooks/
      services/
      dashboardSlice.ts
  shared/
    components/
    hooks/
    utils/
Enter fullscreen mode Exit fullscreen mode

Why This Works

  • Everything related to a feature lives together
  • Easier to maintain, debug, and reason about
  • Reduces cross-folder navigation
  • Scales naturally as features grow

Mental Model

Instead of asking, "Is this a component or a hook?", ask: "Which feature does this belong to?"

This approach is now the widely accepted best practice across the React ecosystem. Feature-based structures establish a clear separation of concerns by grouping components, logic, hooks, and related elements within specific feature folders, making the codebase easier to maintain, scale, and collaborate on.


A Production-Ready Folder Structure

Here is a clean, scalable structure you can use in production today:

src/
  app/
    store.ts
    routes.tsx
    queryClient.ts

  features/
    auth/
      components/
        LoginForm.tsx
        SignupForm.tsx
      hooks/
        useAuth.ts
      services/
        authApi.ts
      types/
        authTypes.ts
      index.ts

    posts/
      components/
      hooks/
      services/
      index.ts

  shared/
    components/
      Button.tsx
      Modal.tsx
    hooks/
      useDebounce.ts
    utils/
      formatDate.ts
    constants/
      appConstants.ts

  assets/
  styles/
  main.tsx
Enter fullscreen mode Exit fullscreen mode

This structure works for both small projects and large-scale applications. You can always adjust the specifics depending on the complexity and requirements of your project.


Understanding Each Layer

1. app/ -- Application Core

This is your application's backbone:

  • Global store configuration (Zustand, Redux Toolkit, etc.)
  • Routing setup
  • TanStack Query client configuration
  • App-level providers and configuration

2. features/ -- Business Logic

Each feature is self-contained. Inside a feature folder:

  • UI components specific to that feature
  • API service functions
  • State management slices or stores
  • Custom hooks encapsulating feature logic

This is where most of your development work happens. Keeping feature slices colocated within their own feature folder (e.g., authSlice inside auth/) is the recommended pattern.


3. shared/ -- Reusable Modules

Things that are not tied to any single feature:

  • Generic UI components (buttons, modals, inputs)
  • Utility functions
  • Common hooks used across features

Rule: If multiple features use it, move it to shared/.


4. assets/ and styles/

  • Images, icons, fonts
  • Global stylesheets or Tailwind CSS configuration

Component Design: Keep Them Small and Focused

A common mistake in large applications is creating monolithic components that handle too many responsibilities.

Avoid This

function Dashboard() {
  // 500 lines of logic, API calls, UI rendering...
}
Enter fullscreen mode Exit fullscreen mode

Do This Instead

Break it down into focused units:

Dashboard/
  Dashboard.tsx
  StatsCard.tsx
  ActivityFeed.tsx
  useDashboard.ts
Enter fullscreen mode Exit fullscreen mode

Each component should:

  • Do one thing well
  • Be easy to test in isolation
  • Be reusable where appropriate

In 2026, developers should write reusable, modular components, and if any component becomes too large to maintain, it should be broken into smaller pieces.


Smart Import Patterns with Path Aliases

Deep relative imports are a maintenance burden:

// Avoid this
import Button from "../../../../../shared/components/Button";
Enter fullscreen mode Exit fullscreen mode

Use Barrel Files

// shared/components/index.ts
export { default as Button } from "./Button";
export { default as Modal } from "./Modal";
Enter fullscreen mode Exit fullscreen mode

Configure Path Aliases in Vite

Vite has become the preferred build tool for modern React applications. Configure path aliases in your vite.config.ts:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

And update tsconfig.json to match:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now your imports become clean and predictable:

import { Button } from "@/shared/components";
import { useAuth } from "@/features/auth/hooks/useAuth";
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use the vite-tsconfig-paths plugin to automatically sync aliases from tsconfig.json into Vite, eliminating the need to maintain them in two places.


Managing State in Large Apps (2026 Landscape)

State management in the React ecosystem has evolved significantly. The biggest shift: server state and client state are now clearly separated disciplines.

The 2026 State Management Stack

State Type Recommended Tool Use Case
Local UI state useState, useReducer Form inputs, toggles, UI flags
Global client state Zustand Theme, sidebar, auth status
Server/async state TanStack Query API data, caching, background sync
Enterprise scale Redux Toolkit Large teams needing strict patterns
Atomic/derived state Jotai Fine-grained, composable state

Zustand has emerged as the most popular choice in 2026 and is the pragmatic default for most React applications. It offers a minimal API, requires no Provider wrapper, and eliminates boilerplate.

For server state, TanStack Query (formerly React Query) handles caching, deduplication, retries, and background sync. Together with Zustand for client state, this combination covers the needs of most applications.

Redux Toolkit remains widely adopted for large enterprise applications where teams need enforced patterns and time-travel debugging. For teams of 10+ developers, the structure it provides pays for itself.

Rule of Thumb

  • Keep state close to where it is used
  • Avoid unnecessary global state
  • Use TanStack Query for anything fetched from an API -- do not store API responses in Redux or Zustand
  • In 2026, effective React state management means less global state, clear separation of server and client state, and simpler, more focused stores

API Layer: Separate Server State from UI

Mixing data fetching directly into components creates tightly coupled, hard-to-test code.

Avoid This

useEffect(() => {
  fetch("/api/posts").then(/* ... */);
}, []);
Enter fullscreen mode Exit fullscreen mode

Do This Instead

Create a dedicated service layer:

// features/posts/services/postsApi.ts
export const getPosts = async (): Promise<Post[]> => {
  const response = await fetch("/api/posts");
  if (!response.ok) throw new Error("Failed to fetch posts");
  return response.json();
};
Enter fullscreen mode Exit fullscreen mode

Then consume it via TanStack Query in a custom hook:

// features/posts/hooks/usePosts.ts
import { useQuery } from "@tanstack/react-query";
import { getPosts } from "../services/postsApi";

export function usePosts() {
  return useQuery({
    queryKey: ["posts"],
    queryFn: getPosts,
  });
}
Enter fullscreen mode Exit fullscreen mode

And use it cleanly in your component:

function PostList() {
  const { data, isLoading, error } = usePosts();

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Benefits

  • Components remain clean and focused on rendering
  • API logic is centralized and testable
  • Caching, retries, and background refetching are handled automatically
  • Consistent error and loading state patterns across the application

Custom Hooks as Your Abstraction Layer

Custom hooks are the primary mechanism for extracting and reusing logic in React.

Instead of repeating fetch-and-state logic across components, encapsulate it:

// features/auth/hooks/useAuth.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { loginUser, fetchCurrentUser } from "../services/authApi";

export function useAuth() {
  const queryClient = useQueryClient();

  const currentUser = useQuery({
    queryKey: ["currentUser"],
    queryFn: fetchCurrentUser,
  });

  const login = useMutation({
    mutationFn: loginUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["currentUser"] });
    },
  });

  return { currentUser, login };
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters

  • Reusable logic across multiple components
  • Cleaner UI components that focus solely on presentation
  • Better separation of concerns
  • Easier unit testing

Naming Conventions That Scale

Consistency in naming reduces cognitive load across a team:

Type Convention Example
Components PascalCase UserCard.tsx
Hooks use prefix useAuth.ts
Services camelCase, descriptive authApi.ts
Utilities camelCase formatDate.ts
Folders lowercase auth, posts
Types/Interfaces PascalCase AuthTypes.ts

Maintain file names that match their default export. If a file exports LoginForm, the file should be named LoginForm.tsx.


Common Mistakes to Avoid

1. Over-Engineering Too Early

Do not build complex architecture for a small application. Start simple and evolve when the complexity demands it. A flat structure works well when you are working with fewer than 15 components.

2. Too Much Global State

Not everything needs to live in a global store. Local state with useState or useReducer is often sufficient. Reserve global state for truly shared concerns.

3. Mixing Business Logic with UI

Keep data transformation, API calls, and complex logic in hooks and services. Components should receive data and render it.

4. No Clear Ownership

If multiple features are modifying the same file, that is a red flag. Each feature should own its files. Shared concerns belong in shared/.

5. Deep Folder Nesting

Deep folder hierarchies cause more problems than they solve. Import paths become unwieldy, and new developers get lost navigating subdirectories. Limit nesting to a maximum of two or three levels.


Scaling Strategy

When your application grows, follow this progression:

  1. Start with a flat structure -- simple and fast for prototypes and small apps
  2. Move to feature-based structure -- when you have more than a handful of distinct features
  3. Extract shared modules -- when duplication across features becomes apparent
  4. Introduce dedicated state management -- when useState and prop drilling no longer suffice
  5. Separate server and client state -- adopt TanStack Query for API data, Zustand or Redux Toolkit for client state

Do not jump to step 5 on day one. Let complexity drive architectural decisions.


My Thoughts

A well-structured React project is not about following trends or adopting the latest tool. It is about making your codebase predictable, maintainable, and easy to work with.

The React ecosystem in 2026 has matured significantly. TypeScript adoption continues to grow as teams seek more scalable and maintainable codebases. Build tooling with Vite is now standard. State management has settled into clear patterns. The community has largely converged on feature-based architecture as the scalable default.

If you remember just one thing from this article:

Group by feature, not by file type.

That single decision will save you hours of confusion as your application grows.


Quick Checklist

  • [ ] Are files grouped by feature instead of type?
  • [ ] Is reusable code extracted into shared/?
  • [ ] Are components small, focused, and single-responsibility?
  • [ ] Is API logic separated from UI via a service layer?
  • [ ] Are you using TanStack Query for server state instead of manual useEffect fetching?
  • [ ] Are imports clean using path aliases (@/)?
  • [ ] Is client state kept close to where it is used?
  • [ ] Is folder nesting limited to two or three levels?
  • [ ] Are naming conventions consistent across the project?

If you get this right early, scaling your frontend becomes significantly less painful -- and considerably more enjoyable.


Found this useful? Follow me for more practical frontend architecture guides. Drop your questions or your own structural patterns in the comments below.

Top comments (0)