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.
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.