CAMERON WESTLAND

My Corner of the Web

Leveraging AI for Smart Code Refactoring

Date:

As a product engineer, I've discovered how generative AI is transforming my code refactoring process. I'm not claiming AI is a cure-all, but I want to share a practical example that many developers facing messy component states will understand.

The Setup: A Tale of State Management Gone Wild

I was working on a multi-step form component "PinContent" when I realized my simple implementation had become a maze of useState declarations and if statements.

import React, { useState } from 'react';
interface UserData {
username: string;
email: string;
avatarUrl?: string;
bio?: string;
}
export const ProfileSetup: React.FC = () => {
// Multiple state declarations for different steps
const [isEmailVerified, setIsEmailVerified] = useState(false);
const [isUsernameValid, setIsUsernameValid] = useState(false);
const [showAvatarUpload, setShowAvatarUpload] = useState(false);
const [isAvatarUploaded, setIsAvatarUploaded] = useState(false);
const [showBioForm, setShowBioForm] = useState(false);
const [isBioComplete, setIsBioComplete] = useState(false);
// Form data
const [email, setEmail] = useState('');
const [username, setUsername] = useState('');
const [avatarUrl, setAvatarUrl] = useState<string>();
const [bio, setBio] = useState('');
const handleEmailSubmit = async () => {
// Simulated email verification
const isValid = await validateEmail(email);
setIsEmailVerified(isValid);
if (isValid) {
setShowAvatarUpload(true);
}
};
const handleUsernameSubmit = async () => {
// Simulated username validation
const isValid = await checkUsername(username);
setIsUsernameValid(isValid);
if (isValid && isEmailVerified) {
setShowAvatarUpload(true);
}
};
const handleAvatarUpload = (url: string) => {
setAvatarUrl(url);
setIsAvatarUploaded(true);
setShowBioForm(true);
};
const handleBioSubmit = () => {
setIsBioComplete(true);
};
const handleSubmitAll = () => {
if (isEmailVerified && isUsernameValid && isAvatarUploaded && isBioComplete) {
const userData: UserData = {
username,
email,
avatarUrl,
bio,
};
submitToServer(userData);
}
};
return (
<div className="profile-setup">
{!isEmailVerified && (
<EmailForm
email={email}
onEmailChange={setEmail}
onSubmit={handleEmailSubmit}
/>
)}
{isEmailVerified && !isUsernameValid && (
<UsernameForm
username={username}
onUsernameChange={setUsername}
onSubmit={handleUsernameSubmit}
/>
)}
{isUsernameValid && showAvatarUpload && !isAvatarUploaded && (
<AvatarUpload onUpload={handleAvatarUpload} />
)}
{isAvatarUploaded && showBioForm && !isBioComplete && (
<BioForm
bio={bio}
onBioChange={setBio}
onSubmit={handleBioSubmit}
/>
)}
{isBioComplete && (
<button onClick={handleSubmitAll}>Complete Setup</button>
)}
</div>
);
};
// Mock functions
const validateEmail = async (email: string): Promise<boolean> => true;
const checkUsername = async (username: string): Promise<boolean> => true;
const submitToServer = (userData: UserData): void => {};
view raw before.tsx hosted with ❤ by GitHub

This is a common situation - requirements evolve, and our components grow messier over time. I needed to add a new step between user validation and profile picture selection, leaving me with two options:

  1. Add more useState declarations and hope for the best
  2. Use AI to help refactor the entire component

The Golden Rule of AI-Assisted Refactoring

Here's a critical tip: Always commit your working changes before starting any AI-assisted refactoring. I've learned this lesson through experience. Without a committed baseline, you risk losing your last working state if the AI suggestions don't pan out.

Enter Cursor: My AI Pair Programming Companion

I currently use Cursor with Claude Sonnet 3.5. While the tool offers various features, I've found Composer handles 75% of my AI interactions, especially for major refactoring work.

The Composer Workflow

Here's my process:

  1. Launch Composer (Command + I)
  2. Set the Context (select target file)
  3. Start with a simple prompt: "The state is spread throughout this component, could you help me consolidate it?"

Cursor presents changes as a diff, similar to a pull request review. This familiar format makes the process more intuitive and manageable.

The AI's Solution: From Boolean Chaos to State Machine

Across different AI models, the suggested approach remains consistent:

  1. Create an enum for primary form states
  2. Replace multiple boolean states with a single state machine
  3. Transform boolean checks into state matching
import React, { useReducer } from 'react';
interface UserData {
username: string;
email: string;
avatarUrl?: string;
bio?: string;
}
// Define possible form states
enum SetupState {
EMAIL_VERIFICATION = 'EMAIL_VERIFICATION',
USERNAME_VALIDATION = 'USERNAME_VALIDATION',
AVATAR_UPLOAD = 'AVATAR_UPLOAD',
BIO_ENTRY = 'BIO_ENTRY',
COMPLETE = 'COMPLETE',
}
// Define state shape
interface SetupFormState {
currentState: SetupState;
data: {
email: string;
username: string;
avatarUrl?: string;
bio?: string;
};
}
// Define possible actions
type SetupAction =
| { type: 'SET_EMAIL'; payload: string }
| { type: 'EMAIL_VERIFIED' }
| { type: 'SET_USERNAME'; payload: string }
| { type: 'USERNAME_VERIFIED' }
| { type: 'AVATAR_UPLOADED'; payload: string }
| { type: 'SET_BIO'; payload: string }
| { type: 'BIO_COMPLETED' };
// Initial state
const initialState: SetupFormState = {
currentState: SetupState.EMAIL_VERIFICATION,
data: {
email: '',
username: '',
},
};
// Reducer function
function setupReducer(state: SetupFormState, action: SetupAction): SetupFormState {
switch (action.type) {
case 'SET_EMAIL':
return {
...state,
data: { ...state.data, email: action.payload },
};
case 'EMAIL_VERIFIED':
return {
...state,
currentState: SetupState.USERNAME_VALIDATION,
};
case 'SET_USERNAME':
return {
...state,
data: { ...state.data, username: action.payload },
};
case 'USERNAME_VERIFIED':
return {
...state,
currentState: SetupState.AVATAR_UPLOAD,
};
case 'AVATAR_UPLOADED':
return {
...state,
currentState: SetupState.BIO_ENTRY,
data: { ...state.data, avatarUrl: action.payload },
};
case 'SET_BIO':
return {
...state,
data: { ...state.data, bio: action.payload },
};
case 'BIO_COMPLETED':
return {
...state,
currentState: SetupState.COMPLETE,
};
default:
return state;
}
}
export const ProfileSetup: React.FC = () => {
const [state, dispatch] = useReducer(setupReducer, initialState);
const handleEmailSubmit = async () => {
const isValid = await validateEmail(state.data.email);
if (isValid) {
dispatch({ type: 'EMAIL_VERIFIED' });
}
};
const handleUsernameSubmit = async () => {
const isValid = await checkUsername(state.data.username);
if (isValid) {
dispatch({ type: 'USERNAME_VERIFIED' });
}
};
const handleAvatarUpload = (url: string) => {
dispatch({ type: 'AVATAR_UPLOADED', payload: url });
};
const handleBioSubmit = () => {
dispatch({ type: 'BIO_COMPLETED' });
};
const handleSubmitAll = () => {
if (state.currentState === SetupState.COMPLETE) {
submitToServer(state.data);
}
};
// Render current step based on state
const renderCurrentStep = () => {
switch (state.currentState) {
case SetupState.EMAIL_VERIFICATION:
return (
<EmailForm
email={state.data.email}
onEmailChange={(email) => dispatch({ type: 'SET_EMAIL', payload: email })}
onSubmit={handleEmailSubmit}
/>
);
case SetupState.USERNAME_VALIDATION:
return (
<UsernameForm
username={state.data.username}
onUsernameChange={(username) =>
dispatch({ type: 'SET_USERNAME', payload: username })
}
onSubmit={handleUsernameSubmit}
/>
);
case SetupState.AVATAR_UPLOAD:
return <AvatarUpload onUpload={handleAvatarUpload} />;
case SetupState.BIO_ENTRY:
return (
<BioForm
bio={state.data.bio || ''}
onBioChange={(bio) => dispatch({ type: 'SET_BIO', payload: bio })}
onSubmit={handleBioSubmit}
/>
);
case SetupState.COMPLETE:
return <button onClick={handleSubmitAll}>Complete Setup</button>;
}
};
return <div className="profile-setup">{renderCurrentStep()}</div>;
};
// Mock functions
const validateEmail = async (email: string): Promise<boolean> => true;
const checkUsername = async (username: string): Promise<boolean> => true;
const submitToServer = (userData: UserData): void => {};
view raw after.tsx hosted with ❤ by GitHub

The Reality Check: AI Isn't Perfect

The AI sometimes misses important details. For example, it struggled with TypeScript's type guards in my recent refactoring. I had a cashTag variable (string or null) with specific states guaranteeing non-null values.

The AI's initial refactoring broke these guarantees.

So here is what I do instead:

  1. Get initial AI refactoring
  2. See if there are type errors or linting issues
  3. Ask AI to fix them. In Cursor it has that context.
  4. Repeat if needed

This process works about half the time - not perfect, but valuable given the task complexity.

The Time-Value Proposition

This particular refactoring would typically take me 5-10 minutes with careful attention. Using AI, I completed the same task in about 30 seconds, plus 1-2 minutes for fixing type errors. Even with some manual fixes needed, the time savings are substantial.

Closing Thoughts

AI tools like Cursor are reshaping both our speed and approach to software development problems. They complement human judgment with fast prototyping and refactoring capabilities.

Looking ahead, I wonder how these tools will evolve to better understand our code's implicit guarantees and patterns. The AI-assisted development journey is just beginning, and its potential is exciting.