DEV Community

Cover image for Stop Writing Spaghetti Code: How I Restructured a 5000-Line React App in One Weekend
Teguh Coding
Teguh Coding

Posted on

Stop Writing Spaghetti Code: How I Restructured a 5000-Line React App in One Weekend

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
  • useEffect grows 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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');
  },
};
Enter fullscreen mode Exit fullscreen mode

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)