React Vertical Slice Architecture: Folder Structure, Features & Best Practices
Stop jumping between six different folders to understand one feature. Learn how vertical slice architecture keeps your React Native code organized by business capabilities instead of technical layers, with real examples you can follow.
Project organization is one of those universal problems that seems simple until your app grows beyond a few screens. Doesn’t matter if you’re using React Native, Flutter, or native development. You start with components in one folder, services in another, and everything feels clean. Then you add more features and suddenly you’re jumping between six different folders just to understand how user authentication works.
I’ve been building the frontend part of my SaaS in React Native, and after trying the usual folder-by-type approach, I switched to vertical slice architecture. The difference in maintainability has been night and day.
Here’s a practical guide to implementing vertical slice architecture in React Native, with a real example you can follow along with.
The Problem with Folder-by-Type
Most React Native tutorials teach you to organize code like this:
src/
components/
services/
utils/
types/
Picture this: you’re building a user profile feature. Your ProfileScreen component goes in components/, your API calls go in services/, any helper functions go in utils/, and your TypeScript interfaces go in types/. Four different folders for one feature.
Now imagine you need to debug why profile updates aren’t working. You’re bouncing between folders like you’re playing pinball. Component renders wrong? Check components/. API call failing? Jump to services/. Data transformation broken? Time for utils/. Missing a property? Visit types/.
Your brain has to maintain a mental map of how these four different pieces connect, and they’re scattered across your entire codebase.
Vertical Slice: Keep Related Things Together
Vertical slice architecture flips this around. Instead of organizing by what things are technically, organize by what they do business-wise.
src/
├── features/
│ ├── auth/
│ ├── profile/
│ ├── settings/
│ └── posts/
├── shared/
│ ├── components/
│ └── utils/
└── navigation/
Each feature folder contains everything needed for that specific capability. When you’re working on profiles, you stay in the profile folder. When something breaks, you know exactly where to look.
Think of it like organizing your kitchen. You could put all plates in one cabinet, all cups in another, all bowls somewhere else. Or you could put everything for making coffee in one spot. The coffee approach is vertical slice.
Building a Profile Feature: Start to Finish
Let’s walk through building a complete user profile feature. We’ll create everything you need: API calls, state management, and UI components. But here’s the twist: it’s all going to live in one folder.
Setting Up the Feature Structure
Create this folder structure:
src/features/profile/
├── ProfileScreen.tsx
├── useProfile.ts
├── profileApi.ts
└── index.ts
Each file has a clear job. The screen handles what users see and interact with. The hook manages data and business logic. The API file talks to your server. The index file is your clean export point.
This isn’t random organization. It’s the classic separation of concerns, just grouped by feature instead of scattered across your project.
The API Layer: Keep It Simple
Let’s start with profileApi.ts:
export const profileApi = {
getProfile: async () => {
const response = await fetch('/api/profile');
return response.json();
},
updateProfile: async (name, email) => {
const response = await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email }),
});
return response.json();
},
};
Notice what’s missing here. No complex error handling, no retry logic, no TypeScript interfaces. We’re focusing on the architecture pattern, not building production code.
The API layer is separate because you might need these same calls from different parts of your feature. Maybe your profile screen needs to load data, but you also have a quick-edit modal that updates the same information.
State Management with Custom Hooks
Now create useProfile.ts:
import { useState, useEffect } from 'react';
import { profileApi } from './profileApi';
export function useProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const loadProfile = async () => {
setLoading(true);
const profile = await profileApi.getProfile();
setUser(profile);
setLoading(false);
};
const updateProfile = async (name, email) => {
const updatedUser = await profileApi.updateProfile(name, email);
setUser(updatedUser);
};
useEffect(() => {
loadProfile();
}, []);
return {
user,
loading,
updateProfile,
};
}
This hook is doing something clever. It’s creating a boundary between “how to manage profile data” and “how to display profile data.” Your UI components don’t need to know about API calls or state management. They just get clean data and simple functions.
This means you could completely change how you fetch data (maybe switch from REST to GraphQL) and your components wouldn’t need to change at all.
The UI Component: Just Focus on Rendering
Create ProfileScreen.tsx:
import React, { useState } from 'react';
import { View, Text, TextInput, Button } from 'react-native';
import { useProfile } from './useProfile';
export function ProfileScreen() {
const { user, loading, updateProfile } = useProfile();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
React.useEffect(() => {
if (user) {
setName(user.name);
setEmail(user.email);
}
}, [user]);
const handleSave = () => {
updateProfile(name, email);
};
if (loading) {
return <Text>Loading...</Text>;
}
return (
<View>
<Text>Profile</Text>
<TextInput value={name} onChangeText={setName} placeholder="Name" />
<TextInput value={email} onChangeText={setEmail} placeholder="Email" />
<Button title="Save" onPress={handleSave} />
</View>
);
}
Look how clean this component is. It’s not worried about where data comes from or how it gets saved. It just handles user interactions and renders the current state.
The component has its own local state for the form inputs, but the real source of truth lives in the hook. This gives you the best of both worlds: responsive UI interactions and centralized data management.
Clean Exports with index.ts
Create index.ts:
export { ProfileScreen } from './ProfileScreen';
export { useProfile } from './useProfile';
This file creates a clean interface for your feature. Other parts of your app can import what they need without knowing about your internal file structure. It’s like having a front door instead of making people climb through windows.
Using Your Feature
Now you can use your profile feature anywhere in your app:
import { ProfileScreen } from '../features/profile';
// In your navigator
<Stack.Screen name="Profile" component={ProfileScreen} />
Or maybe you want to show the user’s name in a header component:
import { useProfile } from '../features/profile';
function HeaderComponent() {
const { user } = useProfile();
return <Text>Hello, {user?.name}!</Text>;
}
The header component doesn’t need to know anything about API calls or state management. It just uses the hook and gets clean data.
Shared Code: The Supporting Cast
Not everything belongs in a feature slice. Some things truly are shared across your entire app.
Create a shared folder for genuinely reusable code:
src/shared/
├── components/
│ └── Button.tsx
├── utils/
│ └── validation.ts
└── hooks/
└── useStorage.ts
For example, a shared button component:
import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
export function Button({ title, onPress, disabled }) {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<Text>{title}</Text>
</TouchableOpacity>
);
}
Here’s where I’m going to break one of the sacred rules of programming: a little code duplication is completely fine.
I know, I know. “Don’t repeat yourself” has been drilled into every developer’s head since day one. But honestly? I’d rather have the same button component copied into two features than create a shared component that tries to handle every possible use case with a bunch of props and configuration options.
When you have a simple button that’s used in both auth and profile features, duplicating it means each feature can evolve independently. Maybe the auth button needs to handle loading states differently, or the profile button needs specific styling. With separate components, you just change what you need without worrying about breaking the other feature.
The magic number for me is three. If I find the same code in three different features, then it’s time to move it to shared. Before that? Duplication is often the better choice.
This goes against everything you’ve been taught, but shared components come with hidden costs. They create dependencies between features. They accumulate complexity over time as different features need slightly different behaviors. They make features less portable.
Start with duplication. Embrace it. Move to shared components only when the pain of maintaining duplicated code outweighs the pain of managing shared dependencies.
The Mental Model Shift
Traditional folder-by-type organization makes you think like a computer: “Where do components go? Where do services go?” You’re organizing by technical categories.
Vertical slice makes you think like a user: “I want to work on the profile feature. Where does profile stuff live?” You’re organizing by business capabilities.
This mental model shift has practical benefits. When someone reports a bug with profile updates, you immediately know to look in the profile folder. When you need to add a new profile-related feature, you know exactly where it belongs.
Compare this to folder-by-type, where adding a profile feature might require touching files in four different directories. You end up with related code scattered across your entire project.
Vertical Slice vs Feature-Based Architecture
Feature-based architecture says: group your code by feature. Auth stuff goes in auth/, profile stuff goes in profile/. That’s it. The organizational unit is the feature, not the technical role. Sound familiar? It should, because vertical slice starts from the same instinct.
Feature-based architecture is primarily a folder structure decision. You group related screens and components by feature, but your state management, API clients, and business logic might still live in centralized layers outside those folders. It’s better than folder-by-type, but features can still have dependencies reaching outside their boundaries.
Vertical slice goes further. Each slice should be a self-contained unit that cuts through every architectural layer: UI, business logic, and data access all live inside the feature folder.
Here’s where you feel the difference:
Feature-based:
src/
├── features/
│ ├── profile/
│ │ ├── ProfileScreen.tsx
│ │ └── ProfileCard.tsx
│ └── auth/
│ ├── LoginScreen.tsx
│ └── RegisterScreen.tsx
├── services/
│ └── apiClient.ts
├── store/
│ ├── profileSlice.ts
│ └── authSlice.ts
└── hooks/
└── useAuth.ts
Vertical slice:
src/
├── features/
│ ├── profile/
│ │ ├── ProfileScreen.tsx
│ │ ├── ProfileCard.tsx
│ │ ├── useProfile.ts
│ │ ├── profileApi.ts
│ │ └── index.ts
│ └── auth/
│ ├── LoginScreen.tsx
│ ├── RegisterScreen.tsx
│ ├── useAuth.ts
│ ├── authApi.ts
│ └── index.ts
└── shared/
└── components/
In the feature-based version, your state management and API logic still live outside the feature folder. You know where the screen is, but understanding the full feature still requires a field trip to store/ and services/.
In the vertical slice version, everything lives together. Open the folder, read the code, close the folder. Done.
Where they overlap: Both reject flat folder-by-type. Both group related screens and components by business capability. For small features with minimal state, they look basically identical.
When feature-based is enough: If your state management is centralized by design - maybe you’re using Redux Toolkit with a global store and that’s working fine - forcing each feature to own its state might create more friction than it solves. Feature-based gives you the organizational benefit without requiring you to rethink your data flow.
When vertical slice is worth the extra structure: When features need to be independently testable, replaceable, or understandable by someone who joined the team recently. If a new developer can open one folder and understand the entire feature without reading anything else, you’ve done it right.
| Feature-Based | Vertical Slice | |
|---|---|---|
| Primary concern | Folder organization | Feature independence |
| State management | Often centralized | Owned by the feature |
| API calls | May live in shared services | Live inside the feature |
| Best for | Teams already invested in global state | Teams wanting self-contained features |
| Risk | Features still depend on outside layers | Can lead to duplication early on |
Vertical Slice vs Clean Architecture
Clean Architecture is the Uncle Bob approach. The core rule: dependencies always point inward. Your business logic should never know about React Native, your API framework, or your navigation library. Everything gets abstracted behind interfaces so the inner layers stay pure.
In a React Native project, Clean Architecture often produces something like this:
src/
├── domain/
│ ├── entities/
│ │ └── User.ts
│ └── useCases/
│ └── GetUserProfile.ts
├── data/
│ ├── repositories/
│ │ └── ProfileRepositoryImpl.ts
│ └── datasources/
│ └── ProfileRemoteDataSource.ts
├── presentation/
│ ├── screens/
│ │ └── ProfileScreen.tsx
│ └── viewmodels/
│ └── ProfileViewModel.ts
└── di/
└── container.ts
That’s a lot of files for a profile screen.
Clean Architecture has real strengths. The dependency rule means your business logic is genuinely portable. You could, in theory, swap out React Native for a different framework without touching your domain layer. Your use cases are testable without any UI or network dependencies. For apps with complex business rules, that isolation is worth the ceremony.
But here’s the thing: most React Native apps don’t have complex domain logic. They fetch data from an API, display it, let the user change it, and send it back. Adding repository interfaces, use case classes, and a dependency injection container for that workflow is like wearing a suit to do your laundry. Technically correct attire, but wildly inappropriate for the task.
| Vertical Slice | Clean Architecture | |
|---|---|---|
| Organizing principle | Feature cohesion | Dependency direction |
| Abstraction level | Pragmatic (hook + API + component) | Formal (entities, use cases, repositories, adapters) |
| File count per feature | 3–5 files | 6–10+ files |
| Testability | Good (test hooks and API separately) | Excellent (pure domain logic, no framework deps) |
| Best for | Data-driven CRUD apps, small-to-mid teams | Domain-heavy apps, shared business logic across platforms |
| Learning curve | Low | Significant |
| Risk | Business logic can leak into components | Over-engineering simple features |
One more thing. You’ll occasionally see people combine these approaches: Clean Architecture within each vertical slice. The feature folder contains its own domain, data, and presentation layers. This can work for larger apps with genuinely complex features, but be honest with yourself about whether the complexity is serving the project or just making the architecture diagram look impressive in a pull request.
Vertical Slice vs Layered Architecture
Layered architecture is probably what you’re already doing, just with a formal name. It’s the pattern behind most tutorial-style React Native projects.
The idea: split your app into horizontal layers, each with a clear responsibility. Presentation on top, business logic in the middle, data access at the bottom. Each layer only talks to the one directly below it.
If you’ve ever structured a project like this, you’ve done layered architecture:
src/
├── screens/ ← presentation layer
├── hooks/ ← business logic layer
├── services/ ← data access layer
├── models/ ← data models
└── utils/ ← shared utilities
The earlier section on folder-by-type already covered why this gets painful as your app grows. But layered architecture is worth addressing separately because the formal version adds something the casual folder structure doesn’t: dependency rules. Presentation depends on business logic. Business logic depends on data access. Never upward, never sideways.
In theory, that’s clean. In practice, those rules erode fast. Your screens import directly from services. Your hooks import from models, which import from utils, which get used everywhere. The clean dependency graph turns into a suggestion, and nobody notices until a refactor touches everything.
There’s also a subtler problem. In a layered project, feature changes rarely stay in one layer. Adding a new field to user profiles means updating the model, the service, the hook, and the screen. Four files, four folders, one feature change. Every modification is a vertical cut through horizontal layers… Which is exactly what vertical slice architecture makes explicit.
Vertical slice solves this by making that vertical cut the organizing principle instead of an accident. Instead of slicing through layers every time you work on a feature, you start with the feature and contain everything inside it.
Where they overlap: Vertical slice doesn’t abandon layers. It scopes them to individual features. Inside a vertical slice, you still have a component (presentation), a hook (logic), and an API file (data). The layers exist. They’re just local instead of global.
When layered architecture makes sense: For small apps with a handful of screens, the overhead of feature folders is unnecessary. If your entire app is five screens and a settings page, a simple layered structure keeps things flat and easy to navigate. Layered architecture also works when most features genuinely share the same data layer. An app where almost every screen reads from the same core data source, and the service layer is doing real work that shouldn’t be duplicated.
| Layered Architecture | Vertical Slice | |
|---|---|---|
| Organizes by | Technical responsibility | Business capability |
| A feature change touches | Multiple folders across layers | One feature folder |
| Dependency direction | Top-down (presentation → logic → data) | Inward (features depend on shared, not on each other) |
| Team scaling | Developers step on each other’s files | Developers own features independently |
| Best for | Small apps, heavily shared data layers | Multi-feature apps, growing teams |
| Risk | Cross-feature coupling creeps in silently | Duplication before shared patterns emerge |
When Vertical Slice Shines
This approach works best when your app has distinct business features that don’t share complex state. Think about apps like:
- Social media (posts, profiles, messaging, notifications)
- E-commerce (products, cart, checkout, account)
- Productivity tools (documents, settings, sharing, collaboration)
Each of these features is conceptually separate. They might interact, but they have clear boundaries.
Vertical slice might be overkill for very simple apps with just a few screens. But as your project grows, keeping related code together becomes incredibly valuable.
Common Pitfalls and How to Avoid Them
Feature coupling. If you find features importing from each other frequently, you probably need to rethink your boundaries. Either move the shared code to the shared folder, or reconsider how you’ve divided your features.
Overly granular features. Don’t create a feature for every single screen. Group related functionality together. “User management” is probably better than separate “edit profile” and “view profile” features.
Premature optimization. Don’t try to predict what will be shared. Start with code in features and move it to shared when you actually need to duplicate it.
The key insight is that organization should serve your development workflow, not the other way around. If you find yourself constantly jumping between folders to understand one feature, your organization isn’t working.
Vertical slice architecture keeps related code together, making your React Native projects easier to understand, maintain, and extend. Once you try it, going back to folder-by-type feels like organizing your kitchen by putting all round things in one cabinet and all square things in another.