How to Refactor a Messy React Component Step by Step

We’ve all been there. A component starts its life as a clean, 30-line file. Then, feature requests roll in. A random useEffect is added to sync some data. A couple of complex conditional ternaries creep into the JSX. A few analytic tracking statements get pasted inside a click handler.

Suddenly, you’re staring at a 400-line monster. It’s hard to read, terrifying to test, and a single change might break three unrelated features.

Let’s take a realistic look at a messy, bloated React component and refactor it step-by-step into clean, maintainable, production-grade code.


The “Before”: The Kitchen-Sink Component

Take a look at this UserProfile component. It handles data fetching, local state management, complex conditional UI rendering, and business logic processing—all in one place.

JavaScript

// UserProfile.jsx (The Messy Version)
import React, { useState, useEffect } from 'react';
import axios from 'axios';

export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState({ name: '', email: '' });

  useEffect(() => {
    setLoading(true);
    axios.get(`/api/users/${userId}`)
      .then(res => {
        setUser(res.data);
        setFormData({ name: res.data.name, email: res.data.email });
        setLoading(false);
      })
      .catch(err => {
        console.error(err);
        setLoading(false);
      });
  }, [userId]);

  const handleSave = async () => {
    // Analytics tracking mixed with business logic
    if (window.analytics) {
      window.analytics.track('Clicked Save Profile', { userId });
    }

    try {
      const res = await axios.put(`/api/users/${userId}`, formData);
      setUser(res.data);
      setIsEditing(false);
    } catch (err) {
      alert('Failed to save profile updates.');
    }
  };

  if (loading) return <div>Loading secure profile data...</div>;
  if (!user) return <div>User record not found.</div>;

  return (
    <div className="profile-container">
      <h1>User Settings</h1>
      {isEditing ? (
        <div className="edit-form">
          <input 
            value={formData.name} 
            onChange={e => setFormData({ ...formData, name: e.target.value })} 
          />
          <input 
            value={formData.email} 
            onChange={e => setFormData({ ...formData, email: e.target.value })} 
          />
          <button onClick={handleSave}>Save Changes</button>
          <button onClick={() => setIsEditing(false)}>Cancel</button>
        </div>
      ) : (
        <div className="profile-view">
          <p><strong>Name:</strong> {user.name}</p>
          <p><strong>Email:</strong> {user.email}</p>
          <p><strong>Account Status:</strong> {user.isActive ? 'Active Member' : 'Suspended'}</p>
          {user.role === 'admin' && <span className="badge">Administrator Privileges</span>}
          <button onClick={() => setIsEditing(true)}>Edit Profile</button>
        </div>
      )}
    </div>
  );
}

Why is this component a technical liability?

  1. Tight Coupling: The component is tightly coupled to the axios network client. If you migrate your app to use GraphQL or a standard fetch wrapper, you’ll have to dig inside your UI files to update the logic.

  2. State Management Overhead: Managing four distinct state hooks in a single component causes unnecessary re-renders and makes tracing state changes mentally exhausting.

  3. Bloated JSX: The return block contains deeply nested ternary operators and logic flags (user.role === 'admin'). This hurts scannability.


The Strategy: Separation of Concerns

To clean this up, we are going to apply three core engineering principles:

  • Custom Hooks: Abstract the data-fetching and form-handling logic out of the UI layer entirely.

  • Component Splitting: Divide the UI into small, focused sub-components.

  • Declarative Conditional Rendering: Clean up the nested ternaries for easier reading.


The “After”: Elegant, Decoupled Architecture

First, we isolate the behavior into a highly reusable, testable custom hook:

JavaScript

// useUserProfile.js (Custom Hook)
import { useState, useEffect } from 'react';
import { fetchUserById, updateUserProfile } from '../services/api';

export function useUserProfile(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState({ name: '', email: '' });

  useEffect(() => {
    let isMounted = true;
    
    async function loadData() {
      try {
        const data = await fetchUserById(userId);
        if (isMounted) {
          setUser(data);
          setFormData({ name: data.name, email: data.email });
        }
      } catch (err) {
        console.error('Error loading profile data:', err);
      } finally {
        if (isMounted) setLoading(false);
      }
    }

    loadData();
    return () => { isMounted = false; };
  }, [userId]);

  const saveProfile = async () => {
    try {
      const updatedUser = await updateUserProfile(userId, formData);
      setUser(updatedUser);
      setIsEditing(false);
    } catch (err) {
      throw new Error('Update failed');
    }
  };

  return {
    user,
    loading,
    isEditing,
    formData,
    setFormData,
    setIsEditing,
    saveProfile
  };
}

Now, look at how beautifully simple our main component file becomes when we plug in our hook and smaller presentational components:

JavaScript

// UserProfile.jsx (Refactored UI Wrapper)
import React from 'react';
import { useUserProfile } from './useUserProfile';
import ProfileView from './ProfileView';
import ProfileForm from './ProfileForm';

export default function UserProfile({ userId }) {
  const { 
    user, 
    loading, 
    isEditing, 
    formData, 
    setFormData, 
    setIsEditing, 
    saveProfile 
  } = useUserProfile(userId);

  if (loading) return <div className="status-message">Loading secure profile data...</div>;
  if (!user) return <div className="status-message">User record not found.</div>;

  return (
    <div className="profile-container">
      <h1>User Settings</h1>
      {isEditing ? (
        <ProfileForm 
          formData={formData} 
          onChange={setFormData} 
          onSave={saveProfile} 
          onCancel={() => setIsEditing(false)} 
        />
      ) : (
        <ProfileView 
          user={user} 
          onEditClick={() => setIsEditing(true)} 
        />
      )}
    </div>
  );
}

(For maximum neatness, ProfileView and ProfileForm are split out into their own tiny, lightweight functional component files that just accept raw props).


The Impact of the Refactor

Metric Before After
Main UI File Length ~65 lines ~26 lines
Testability High difficulty (Requires API mocking inside UI tests) Extremely easy (Test hook logic and pure UI props separately)
Readability Poor (Cognitive overload from mixed logic) High (Reads like a simple blueprint)

Key Takeaways for Your Next Refactor

  1. Keep UI Dumb: Your main JSX files should focus strictly on how things look, not how things work.

  2. Extract Side-Effects immediately: If you see an API call, analytics ping, or local-storage sync sitting inside your component wrapper, strip it out into a custom hook or an external service module.

  3. Value Scannability over Density: Writing compact, compressed inline logic might feel satisfying in the moment, but split sub-components pay dividends when a teammate needs to update the code six months from now.

Leave a Comment