adjoe Engineers’ Blog
 /  Frontend  /  Moving to Feature-Based React Architecture
Chaos to Cohesion: Moving to Feature-Based React Architecture
Frontend

Chaos to Cohesion: Moving to Feature-Based React Architecture

When we first built the adjoe advertiser dashboard, our goal was to give mobile app advertisers a clear view of campaign performance. At that point, we kept our file structure quite simple: components in one folder, hooks in another one, types and constants in their places.

Over the past year, adjoe became one of the largest rewarded ad networks, and our tech team doubled in size to support that growth. As we added advanced reporting, multi-campaign management, and detailed KPI analytics, the dashboard quickly grew complex.

Consequently, the React project swelled to 100K+ lines of code, 50+ files per folder, and some files over 1000 lines. Even small edits turned into time-consuming treasure hunts.

We realized our type-based file structure was holding us back. It slowed development and made onboarding new developers difficult.

That’s why we moved to a feature-based react architecture. In this post, we explore the bottlenecks we found on type-based and how we built an architecture that could scale effectively.

Why Type-Based React Structure Fails to Scale?

As the type-based structure no longer scaled, we had to clarify what was wrong with it. After some heated debates, we nailed down what was actually broken.   

1. Low cohesion in folders

Every feature requires searching and touching many layers: src/types, src/const, src/hooks, src/api, src/store, src/components. It leads to a mental overhead and slower delivery.

2. Components are apart from their children

There are two types of components:

  • Blocks: Independent components that encapsulate some functionality, like “invoices table”. It can even be a small one, like a “User activity switch”. But it should be independent and work for a user goal. 

  • Elements: Parts that exist only inside their parents. Like “invoices table row”, it makes no sense to render a table row outside of a table.

But currently, the “Elements” live outside of their parents. We had to create a separate folder for them with just a single tsx file for each one.

3. High coupling across modules 

Types, constant, utils, and hooks folders became “junk drawers” with unclear dependencies. We risk breaking unrelated parts of the app when editing something.

4. Bad components discoverability 

The search space for reusable logic was huge. Miss one helper, write a new one. That’s how slightly different versions of the same function started to multiply.

5. Bad scalability in terms of code 

The longer the project lives, the more files we have. One folder contained 202 files. New developers spent days learning where logic lived. Every change felt like an archaeology project.

6. Dead code stays in the repo

When we removed a component, we often forgot to remove its hooks or constants. Out of sight, out of mind, we just didn’t see them.

7. Testing becomes difficult 

Without isolated features, our tests targeted helpers, not use cases. It was impossible to clearly see the boundaries of responsibilities.

Designing a Scalable React Architecture

Designing a Scalable React Architecture  - Chaos to Cohesion: Moving to Feature-Based React Architecture

After mapping the pain points, we defined a set of rules to organize React features. These core guidelines aim to:

  • Keep related code together.
  • Improve component discoverability.
  • Have a structure that scales quickly as the project grows.
  • Reduce overhead for the team. 

1. Keep coupled code together 

Code that belongs to a component should live with it. Hooks, constants, and child components stay close to reduce confusion and speed up updates.

  1. The hook useInAppEventsOptions is used only in the CampaignEvents.tsx, it should stay in the same directory src/.../CampaignEvents/useInAppEventsOptions.ts, not in the src/hooks or src/campaigns/hooks

  • If the ACCEPTED_FILE_TYPES constant is used only in the UploadInvoices component, it should live in the UploadInvoices/const.ts, not in the src/const or src/invoices/const

  • If a component can’t exist without its parent, keep them close. Instead of having three folders InvoicesTable, InvoicesTableCell, InvoicesTableMobileCell we should have one InvoicesTable directory containing all the files.

2. Features first, details second 

The top level folders show business domains, like users, campaigns, apps, not technical layers. Reusable pieces like AndroidIcon live in src/shared.

3. No bottom-up dependencies 

The API layer shouldn’t know about components, constants shouldn’t know about state management.

Feature-Based File Structure for React App  

With the key principles in place, here’s how the react project is organized. At the top level we have just three folders. Everything else lives inside them:

src/

 ├─ app/

 ├─ features/

 └─ shared/

1. App: Entry Point of a React Project

Holds providers, routes, and pages. Knows everything about the app. Imports are allowed from anywhere! It contains:

  • pages/ — full pages tied to URLs, can mix widgets from different features
  • store/ — global Redux setup (combineReducers, providers)
  • other app-wide configs

2. Features: Where the Business Logic Lives

Each feature reflects a business entity: campaigns, apps, statistics, auth. This is where most of the work happens!

Business entities are naturally linked. A campaign belongs to an app, a user belongs to an organization. So a feature can import modules from other features when needed.

Every feature may contain:

pages/ → the page-root components
ui/    → components, hooks, formatters
store/ → Redux slices and state logic
api/   → endpoints, mappers, API constants
utils/ → helpers shared inside this feature
const/ → feature-wide constants
types/ → feature-wide types

// Rules of thumb:

  1. User interface can reach down to everything below it.
  2. But lower layers never look up!

3. Shared: Reusable Components and Utilities

A place for common components and hooks not tied to any specific domain, or some set ups that needed to be shared between features.

PlatformIcon, formatNumber, PaginatedResponse, global store setup, all live here. Imports from app or features are forbidden here.

Implementation in Action

Let’s cover how we implemented the new structure. We started by grouping features “core” into their own folders. We moved all their pages, components, store, and api code under one roof. 

There were still junk drawers with utils and consts, but we had 80% of the issues gone for 20% of the effort.

Then came “tactical refactoring”. Fixing a bug or adding a feature? Great, but if you spot code living in the wrong place, move it. That stray hook in shared used by one component? Send it home.

With the combination of import aliases the dependencies of components became transparent:

import { Card } from '@/shared/ui/Card'
import { Button } from '@/shared/ui/Button'
import { AppInfo } from '@/features/apps/ui/AppInfo'
import { useCampaignQuery } from '../../api'
export const CampaignPreview = () => {
 const { data: campaign } = useCampaignQuery()
 return (
   <Card>
     <h1>{campaign.name}</h1>
     <AppInfo app={campaign.app} />
     <Button>Edit</Button>
   </Card>
 )
}

Here’s an example of paths we have now:

src/app/
src/app/App.tsx
src/app/store/store.ts - buildStore, persist, etc
src/app/pages/routes.ts - all the routes

src/features/
src/features/apps/
src/features/apps/pages/AppsList.tsx
src/features/apps/pages/EditApp.tsx
src/features/apps/ui/AppsTable/
src/features/apps/ui/AppsTable/AppsTable.tsx
src/features/apps/ui/AppsTable/AppsTableDesktopRow.tsx
src/features/apps/ui/AppsTable/AppsTableCellWrapper.tsx
src/features/apps/ui/EditApp/
src/features/apps/ui/EditApp/EditApp.tsx
src/features/apps/store/
src/features/apps/store/slice.ts
src/features/apps/store/selectors.ts
src/features/apps/api/
src/features/apps/api/appsApi.ts
src/shared/
src/shared/ui/
src/shared/ui/Button/Button.tsx
src/shared/store/
src/shared/store/paginationReducers.ts
src/shared/api/
src/shared/api/rootApi.ts
src/shared/utils/
src/shared/utils/formatNumber.ts

Results: Clean React Architecture for Scalable Apps 

Editing code is calm and fun now. The file tree finally mirrors how the product works, so most changes happen inside a single feature.

Here’s an overview of what we achieved: 

  • Faster edits with fewer cross-folder jumps.
  • Safer refactoring, less risk of breaking code.
  • Quicker onboarding for new developers.
  • Improved component and helper discoverability.
  • Clear boundaries for easier testing.
  • React architecture that scales as we add more features. 

EndNote 

For our frontend team, architecture is about clarity and scalability. When code mirrors the product’s business logic, developers move faster. Clean structure reduces errors, improves maintainability, and ensures the React project can grow without chaos. 

Want more React best practices? Explore front-end topics from the adjoe engineers’ blog and keep up with the latest developer tips. 

Build products that move markets