Prompt of the Day: Refactor a 200-Line Component into Smaller Pieces
Series: Prompt of the Day — Part 4 of 30 | Track: Prompts | By Tom Hundley
The Component That Ate Your Codebase
Six months ago, a developer named Priya started vibe coding a SaaS dashboard with Cursor. She described what she wanted, the AI delivered, and she shipped it. Fast. Clean.
Then she had to add a filter to the data table.
She opened DashboardPage.tsx and found 247 lines of intertwined JSX, six useState calls, three useEffect hooks, two API fetches, a form handler, and a modal—all living in one function. The component had grown like a city without a zoning code. She spent four hours just understanding what she already had before writing a single line.
This is the most common trap in AI-assisted development. As one developer put it in a 2025 thread: "I might receive 200 lines of code that include hooks, providers, validation schemas. Although it works flawlessly, I find myself completely lost when I try to make modifications weeks later." A Lawfare Media analysis of vibe coding risks put it bluntly: "Coding agents often generate new instead of reusing or refactoring existing code, leading to code bloat and technical debt."
The good news: breaking a bloated component apart is exactly the kind of task AI does exceptionally well—if you give it the right instructions.
The Prompt
Refactor the component below into smaller, focused pieces. Follow these rules:
1. Extract each distinct section of JSX into its own component (e.g., <Header />, <FilterBar />, <DataTable />, <EmptyState />). Put each in its own file under components/<ComponentName>/<ComponentName>.tsx with a default export.
2. Extract all data-fetching logic (fetch calls, loading/error state) into a custom hook named use<Resource> (e.g., useOrders, useDashboard). Place it in hooks/use<Resource>.ts. Return { data, isLoading, error } from the hook.
3. Extract any form handlers, validation logic, or transformation functions into a utils file at utils/<feature>Utils.ts. These should be pure functions with no React dependencies.
4. Keep the parent component as a thin orchestrator: it imports the sub-components and hooks, handles layout, and passes props. It should be under 50 lines.
5. Do NOT change any behavior, UI, or business logic. This is a pure structural refactor.
6. After the refactored code, give me a file tree showing exactly where each new file lives.
Here is the component:
[PASTE YOUR COMPONENT HERE]
Why It Works
This prompt succeeds because it does four things a vague refactor request never does:
It gives the AI a decision framework. Without rules, the AI guesses at where to draw the lines. With explicit rules—JSX sections become components, hook logic becomes custom hooks, pure functions become utils—it produces consistent, predictable output.
It enforces the single-responsibility principle structurally. By asking for a file per component and a file per hook, you prevent the AI from "refactoring" by just renaming things inside the same file. Each concern gets a physical home.
It protects against behavior drift. Rule #5 is critical. Without it, the AI often improves your code—refactoring a useEffect, adding error boundaries, simplifying a fetch—which sounds great until you have a subtle regression in a feature you didn't know was coupled.
It produces a file tree. Asking for the output forces the AI to commit to a structure you can review before accepting it. You're auditing architecture, not just syntax.
What the output looks like
Given a bloated DashboardPage.tsx, the prompt produces something like:
Before — one 220-line file:
// DashboardPage.tsx — the monolith
export default function DashboardPage() {
const [orders, setOrders] = useState<Order[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState('all');
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
useEffect(() => {
fetch('/api/orders')
.then(r => r.json())
.then(data => setOrders(data))
.catch(err => setError(err.message))
.finally(() => setIsLoading(false));
}, []);
const filteredOrders = orders.filter(o =>
filter === 'all' ? true : o.status === filter
);
// ... 180 more lines of JSX, handlers, and modal logic
}
After — a clean orchestrator:
// DashboardPage.tsx — 40 lines, easy to scan
import { DashboardHeader } from '@/components/DashboardHeader/DashboardHeader';
import { OrderFilterBar } from '@/components/OrderFilterBar/OrderFilterBar';
import { OrderTable } from '@/components/OrderTable/OrderTable';
import { OrderDetailModal } from '@/components/OrderDetailModal/OrderDetailModal';
import { useOrders } from '@/hooks/useOrders';
import { filterOrders } from '@/utils/orderUtils';
export default function DashboardPage() {
const { orders, isLoading, error } = useOrders();
const [filter, setFilter] = useState('all');
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
const filteredOrders = filterOrders(orders, filter);
if (error) return <ErrorState message={error} />;
return (
<main>
<DashboardHeader />
<OrderFilterBar activeFilter={filter} onFilterChange={setFilter} />
<OrderTable
orders={filteredOrders}
isLoading={isLoading}
onSelectOrder={setSelectedOrder}
/>
{selectedOrder && (
<OrderDetailModal
order={selectedOrder}
onClose={() => setSelectedOrder(null)}
/>
)}
</main>
);
}
As CodeScene's analysis of React component health explains, whenever you see a long list of useState or useEffect hooks in a single component, that's a signal—not a failure, just an opportunity to extract a custom hook that names the concept and groups related state together.
The Anti-Prompt
Don't write this:
Refactor this component to make it cleaner
Why it fails:
"Cleaner" is subjective. Without constraints, the AI will apply its own taste—which often means:
- Renaming variables to be more descriptive (cosmetic, not structural)
- Extracting one helper function while leaving the rest intact
- Adding comments to explain the existing complexity instead of removing it
- Or, worst case: restructuring in a way that changes subtle behavior
You'll get code that looks better on first read but has the same monolith problem. More dangerously, the AI might silently change logic while tidying—and without explicit instruction to preserve behavior, you won't catch it until a user reports a bug.
Vague prompts produce vague results. Architecture decisions require explicit constraints.
Variations
When you can't touch the parent component (legacy codebase):
Refactor the JSX and logic inside this component WITHOUT changing the component's
external interface (same props, same exported name). Extract internal sub-components
as unexported functions in the same file, and pull all useEffect/useState groups
into custom hooks in a sibling hooks/ folder. Keep all current prop signatures intact.
When you want Next.js Server Component boundaries respected:
Refactor this component while preserving Next.js App Router constraints:
- Any component that uses useState, useEffect, or event handlers must be a
Client Component with 'use client' at the top
- Any component that only renders data (no interactivity) should be a
Server Component (no 'use client' directive)
- Data fetching that can move to a Server Component should use async/await
directly, not useEffect + fetch
Show me which components are Server vs Client and why.
When you want a complexity audit first:
Before refactoring, analyze this component and give me:
1. A list of distinct responsibilities this component currently owns
2. Which useState calls are related to each other (group them)
3. Which useEffect calls could become custom hooks
4. Your recommended file structure before writing any code
Wait for my approval before writing any refactored code.
This last variation is powerful. Getting the AI to plan before it codes gives you a checkpoint where you can redirect before it goes down the wrong path.
Your Checklist
- Identify your largest component (check for files over 150 lines in your
app/orcomponents/folder) - Run the audit variation first—get the plan before the code
- Review the proposed file structure and confirm it matches how your team thinks about the feature
- Run the full refactor prompt, paste the component
- Verify the file tree output matches what was proposed
- Open a git diff and confirm: no behavior changed, only structure
- Run your test suite (if you have one) or manually click through the affected UI
- Check that each new custom hook has a name that describes what it does, not how it does it (
useOrders, notuseFetchAndSetOrdersArray)
Ask The Guild
Community prompt for this week:
What's the biggest component you've had to tame—AI-generated or otherwise? How many lines was it, and what finally convinced you to break it apart? Drop your story in the comments, especially if you used a prompt to do it.
Bonus: share the audit output if you ran the "analyze before refactor" variation. What did the AI identify that you hadn't noticed?