Back to Articles
React

How I Structure a Modern React Project

A practical look at how I approach React project structure as applications grow—from components and features to API logic, TypeScript, hooks, state management, naming and keeping complexity under control.

Jorris NyangeSeptember 8, 20269 min read
How I Structure a Modern React Project

One of the things that has changed most in the way I build applications isn't necessarily the technologies I use.

It's how I think about structure.

When I was starting out, I mostly cared about getting the page working.

Create a component.

Add some CSS.

Connect an API.

Fix the error.

Move on.

That approach works surprisingly well when you're building something small.

The problem starts when the application grows.

Suddenly, you have components that are hundreds of lines long, API calls scattered throughout the application, duplicated logic, unclear folder names, and files that become increasingly difficult to find.

At some point, you realise that getting the application to work is only part of the job.

You also need to make it easy to understand, maintain and change.

That's how I've gradually started thinking about React project structure.

Structure Should Follow Responsibility

I don't believe there is one perfect folder structure for every React application.

Different applications have different requirements.

A small portfolio website shouldn't have the same architecture as a large business application.

So rather than starting with a folder structure I've seen on GitHub, I start with a simpler question:

What responsibilities does this application have?

For example:

  • User interface
  • Pages and routing
  • API communication
  • Authentication
  • State management
  • Reusable components
  • Business logic
  • Types
  • Utilities
  • Assets

Once those responsibilities become clear, organising the project becomes much easier.

A Structure I Often Start With

For a React + TypeScript application, I might start with something along these lines:

hljs text
src/ ├── assets/ ├── components/ │ ├── common/ │ ├── forms/ │ └── layout/ ├── features/ │ ├── authentication/ │ ├── users/ │ └── dashboard/ ├── hooks/ ├── layouts/ ├── lib/ ├── pages/ ├── services/ ├── types/ ├── utils/ ├── App.tsx └── main.tsx

This isn't a rule.

It's a starting point.

The important thing isn't whether your folders look exactly like this.

The important thing is that you can look at the project and understand where things belong.

Components Should Have a Reason to Exist

React makes it very easy to create components.

Sometimes too easy.

You can quickly end up with something like:

hljs text
Dashboard.tsx

containing:

  • API requests
  • authentication checks
  • state management
  • form logic
  • tables
  • modals
  • buttons
  • validation
  • business rules
  • loading states

And suddenly you're working with a component that's 700 lines long.

I've learned that when a component starts doing too many things, it's usually time to step back.

Instead of one large component, I might break it into:

hljs text
Dashboard ├── DashboardHeader ├── StatisticsCards ├── RecentTransactions ├── UserTable └── UserModal

Each component now has a clearer responsibility.

That makes the code easier to read and also makes future changes less painful.

But Don't Over-Componentise

There's another mistake that can happen when developers learn this lesson.

You can go too far.

Not every <div> needs its own component.

If I create a component that is only used once, contains five lines of JSX and doesn't represent a meaningful piece of the interface, I've probably made the code harder to understand rather than easier.

For me, the question is:

Does this component represent something meaningful, reusable or independently understandable?

If yes, it probably deserves to exist.

If not, keeping it local may be simpler.

Keep Data Logic Away From UI When Possible

Another thing I pay attention to is where API and data-fetching logic lives.

I don't like having large API calls mixed directly into the JSX of a component.

For example, instead of making a component responsible for everything:

hljs tsx
function Users() { // fetch users // handle errors // transform data // render table }

I prefer separating those responsibilities where it makes sense.

For example:

hljs text
services/ └── userService.ts

could contain API-related functionality.

Then the component can focus more on presenting the data.

This doesn't mean every API request needs its own file.

Again, the goal isn't to create more files.

The goal is to create clear boundaries.

I Treat Types as Part of the Architecture

Using TypeScript has also changed how I structure projects.

Types aren't just there to make errors disappear from the editor.

They can describe how different parts of the application communicate.

For example:

hljs ts
interface User { id: string; name: string; email: string; role: string; }

Now different parts of the application can work with a shared understanding of what a User looks like.

This becomes especially useful when working with APIs.

If the backend returns structured data, having corresponding types on the frontend makes it easier to understand what the application expects.

It also makes refactoring safer.

If the shape changes, TypeScript can help reveal the places that need to be updated.

Hooks Should Solve Real Problems

I also try not to create custom hooks simply because they are available.

A hook should normally encapsulate some meaningful behaviour.

For example:

hljs text
hooks/ ├── useAuth.ts ├── useDebounce.ts └── usePermissions.ts

These are useful because they represent reusable behaviour.

If I create a hook that simply returns a constant or wraps one line of code without adding meaningful abstraction, I've probably created unnecessary complexity.

The same principle applies to utilities.

Abstraction should reduce complexity, not move it somewhere else.

Features Can Become More Important as the Application Grows

One approach I've found particularly useful as applications become larger is organising some code around features rather than only technical categories.

For example:

hljs text
features/ ├── authentication/ │ ├── components/ │ ├── hooks/ │ ├── services/ │ └── types.ts │ ├── users/ │ ├── components/ │ ├── services/ │ └── types.ts │ └── dashboard/ ├── components/ ├── hooks/ └── services/

This can make a large application easier to navigate.

If I'm working on authentication, I don't necessarily want to search through five unrelated folders to find everything related to authentication.

I want the relevant code to be relatively close together.

This is especially useful when different parts of the application have their own business rules and workflows.

Shared Components Are Different

There are usually components that don't belong to one particular feature.

Things like:

hljs text
Button Input Modal Dropdown Card Table Spinner

These can live in a shared component area.

For example:

hljs text
components/ ├── common/ ├── forms/ └── layout/

The idea is simple:

Feature-specific components stay with the feature. Generic reusable components stay shared.

That separation can prevent the shared components folder from becoming a dumping ground for everything in the application.

Don't Build the Architecture You Think You'll Need

This is probably one of the easiest mistakes to make.

You start a project and think:

"This application might eventually have payments, notifications, multiple user roles, analytics, offline support and five different dashboards."

So you build the architecture for all of those things before you've built the actual application.

I've learned to be more careful about that.

Architecture should support the application you have while leaving enough room for it to grow.

There's a difference between planning for growth and building imaginary features.

You don't need a massive architecture on day one.

You need a structure that can evolve.

Naming Matters More Than It Seems

A well-structured application can still be frustrating if everything is poorly named.

Compare:

hljs text
data.ts helper.ts utils2.ts componentNew.tsx finalComponent.tsx

with:

hljs text
userService.ts formatCurrency.ts permissionUtils.ts UserTable.tsx

The second set tells you something before you even open the files.

Good naming reduces the amount of mental work required to understand a codebase.

I try to name things based on what they actually do, not what they happened to be called when I created them.

I Also Think About the Backend

Frontend architecture doesn't exist in isolation.

When an application communicates with a backend, the frontend structure needs to reflect how that communication works.

For example, if an API has concepts such as:

hljs text
Users Roles Permissions Applications Transactions Reports

those concepts will often influence how the frontend is organised.

This is one of the things I appreciate more now than I did when I was starting out.

Building applications isn't just about creating screens.

You have to understand the systems behind those screens.

A page might look simple to a user while representing several different backend operations, permissions and workflows.

The frontend structure needs to make those relationships manageable.

What About State Management?

State management is another area where I try to avoid unnecessary complexity.

Not every application needs a large state-management library.

Sometimes React's built-in state and context are enough.

Sometimes you need something more specialised.

And sometimes the problem isn't really client state at all.

For example, server data and UI state are different problems.

A list of users retrieved from an API isn't necessarily the same kind of state as whether a modal is currently open.

Keeping that distinction in mind can prevent an application from turning into one giant global state object.

The tools can change.

The principle stays the same:

Keep state as close as possible to where it is actually needed.

My Goal Isn't the "Perfect" Structure

The longer I work with software, the less interested I am in finding the perfect folder structure.

There probably isn't one.

A good project structure should make it easier to:

  • Find code
  • Understand code
  • Reuse code
  • Test code
  • Change code
  • Onboard another developer
  • Add new features
  • Remove old features

If the structure helps with those things, it's doing its job.

If it looks impressive but makes the application harder to understand, then it isn't really helping.

How My Thinking Has Changed

When I was starting out, I thought good development mostly meant writing code that worked.

Now I think about the code that comes after the code.

What happens when someone needs to change this six months from now?

What happens when another developer joins the project?

What happens when the API changes?

What happens when this feature grows?

What happens when the application has ten times as many users?

Those questions have changed how I structure applications.

I'm still learning, and I'm sure the way I organise projects will continue to evolve.

But one principle has become increasingly important to me:

Good structure isn't about having more folders. It's about making complexity easier to manage.

That's ultimately what I'm trying to achieve when I structure a React application.

Not perfection.

Not architecture for the sake of architecture.

Just a codebase that makes sense today and gives the next feature somewhere sensible to live tomorrow.

Build for the problem you have. Structure for the growth you can reasonably expect.

Share this article

Found this helpful? Share it with your network.

Newsletter

Enjoyed This Article?

Subscribe to get more insights like this delivered straight to your inbox. No spam, unsubscribe anytime.

By subscribing, you agree to our Privacy Policy.