You know that feeling when you open a file in your own project and think, who wrote this garbage? Then you realize — it was you, three months ago.
That was me, staring at a 5000-line React app I had built solo over six months. It worked. Barely. But adding any new feature felt like defusing a bomb blindfolded.
This is the story of how I restructured it over a single weekend — and what I wish I had known from day one.
The Problem: How Spaghetti Happens
It never starts bad. You start clean, with good intentions. A component here, a hook there. Then:
- Deadlines hit, so you copy-paste instead of refactor
- Business logic leaks into UI components
-
useEffectgrows to 80 lines with six dependencies - State gets passed down seven component levels because "we'll fix it later"
- "Later" never comes
Before you know it, your Dashboard.jsx imports from 14 different places and nobody touches it without a good reason to be afraid.
The Audit: Before Touching Anything
The first thing I did was NOT write code. I spent two hours just reading and mapping.
I created a simple spreadsheet with three columns:
| File | What it does | What it SHOULD do |
|---|---|---|
Dashboard.jsx |
Fetches data, formats dates, renders UI, manages 3 different local states | Just render UI |
useAuth.js |
Auth logic + localStorage + redirect logic | Auth logic only |
utils.js |
200 unrelated helper functions | Split into domains |
This audit took two hours. It saved me from making the wrong cuts.
The Architecture I Landed On
After research and reflection, I adopted a feature-first folder structure instead of the classic type-first approach.
Before (type-first — the classic mess):
src/
components/
Button.jsx
Modal.jsx
UserCard.jsx
ProductCard.jsx
...(40 more files)
hooks/
useAuth.js
useFetch.js
useCart.js
...
utils/
helpers.js // a graveyard of functions
pages/
Home.jsx
Dashboard.jsx
The problem: when working on the "cart" feature, your files are scattered across four folders.
After (feature-first):
src/
features/
auth/
components/
LoginForm.jsx
LogoutButton.jsx
hooks/
useAuth.js
authService.js
index.js
cart/
components/
CartDrawer.jsx
CartItem.jsx
hooks/
useCart.js
cartService.js
index.js
products/
components/
ProductCard.jsx
ProductGrid.jsx
hooks/
useProducts.js
productService.js
index.js
shared/
components/
Button.jsx
Modal.jsx
hooks/
useFetch.js
utils/
date.js
format.js
pages/
Home.jsx
Dashboard.jsx
App.jsx
Now when I work on cart, everything I need is in features/cart/. Pure focus.
The Refactor Rules I Set for Myself
Before touching any code, I wrote down three rules to follow strictly:
Rule 1: Components only render. No data fetching inside components.
// BEFORE — the bad old way
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
// AFTER — clean separation
function UserProfile({ userId }) {
const { user, loading } = useUser(userId); // all logic lives here
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
Rule 2: Custom hooks own the state and logic. Period.
// features/auth/hooks/useAuth.js
export function useAuth() {
const [user, setUser] = useState(null);
const [status, setStatus] = useState('idle');
const login = useCallback(async (credentials) => {
setStatus('loading');
try {
const data = await authService.login(credentials);
setUser(data.user);
setStatus('success');
} catch (err) {
setStatus('error');
}
}, []);
const logout = useCallback(() => {
authService.logout();
setUser(null);
}, []);
return { user, status, login, logout };
}
Rule 3: Services handle API calls. Not hooks, not components.
// features/auth/authService.js
export const authService = {
async login(credentials) {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
if (!res.ok) throw new Error('Login failed');
return res.json();
},
logout() {
localStorage.removeItem('token');
},
};
This three-layer pattern — Service > Hook > Component — was the single biggest improvement I made.
The Weekend Timeline
Saturday morning: Audit and plan. No code. Just reading and mapping.
Saturday afternoon: Set up the new folder structure. Create empty files. Move things that don't need changing yet.
Saturday evening: Refactor auth feature completely. Write tests. Commit.
Sunday morning: Refactor cart feature. Commit. Refactor products.
Sunday afternoon: Clean up shared/ utilities — split that monster utils.js into focused files.
Sunday evening: Fix broken imports, run the app end-to-end, feel like a god.
Total time: roughly 14 hours spread across two days.
What I Learned
1. Structure before you code. The audit was the most valuable part of the whole weekend. Understanding what exists before changing it prevents you from making things worse.
2. Small commits, always. I committed after every feature refactor. If something broke, I could roll back to a known good state without losing hours of work.
3. Tests are your safety net during refactors. I had some basic tests. Where I had tests, I refactored with confidence. Where I did not, I was terrified. Lesson learned.
4. Feature-first scales better than type-first. When your app is small, type-first feels cleaner. When it grows, feature-first keeps things sane. Start feature-first from day one.
5. The Service > Hook > Component pattern eliminates most spaghetti. When every layer has a single job, you always know where to look when something breaks.
One Last Thing
Your future self is going to work on this codebase at 11pm with a deadline tomorrow. Be kind to that person. Structure is not about being perfect — it is about being kind to the next developer, who is probably you.
If your codebase feels overwhelming right now, do the audit first. Just read it. Make the map. The path forward will become obvious.
You do not need a full rewrite. You need a clearer structure and the discipline to maintain it.
Have you done a major refactor that changed how you code? I would love to hear what patterns worked for you — drop it in the comments.
Top comments (0)