Refactoring a 500-Line React useEffect with AI (As the Architect, Not the Passenger)
A five-step prompt workflow I use on legacy admin code: analyze, extract pure logic, test, rewrite orchestration, then add features safely.
Client admin panels accumulate “do not touch” modules. I recently had to add risk checks to an order-submit path buried in a 500+ line useEffect—validation, pricing, coupons, API calls, global state, toasts, all nested in if/else and try/catch.
Manual refactor estimate: about a week for a senior dev, high regression risk. I used AI as a fast executor under a human-owned plan and finished in an afternoon.
Mistake #1: “Refactor this” with no plan
Pasting the whole hook and saying “refactor” gets cosmetic churn—switch instead of if, renamed locals—not design improvement. The model is a strong implementer with no product soul. I own the architecture; it owns the keystrokes.
Five-step workflow
1. Shared understanding (read-only)
Prompt (paraphrased):
You are a senior React architect. For this
useEffect, list: (1) major responsibilities, (2) all side effects, (3) pure logic, (4) maintainability and testability.
Expected output shape:
- Responsibilities: validate form, price cart, check inventory, create order, UI feedback.
- Side effects:
api.post,setLoading,showToast, router navigation. - Pure logic: validators, price calculators.
- Verdict: SRP violated, effectively untestable as one block.
If step 1 is wrong, stop—do not let it edit yet.
2. Extract pure functions first
Refactor only the pure parts into exported TypeScript functions with no side effects—no
fetch, nosetState. Full types on inputs/outputs.
Example targets:
export function validateOrder(form: OrderForm): string | null {
if (!form.user) return "User is required";
if (form.items.length === 0) return "Cart cannot be empty";
return null;
}
export function calculateTotalPrice(items: Item[], coupon: Coupon | null): number {
let total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
if (coupon?.type === "PERCENT") total *= 1 - coupon.value / 100;
return total;
}
Separating calculation from coordination is the hinge. Everything later hangs on it.
3. Make AI prove purity with tests
Write Vitest suites for
validateOrderandcalculateTotalPrice. Cover empty cart, coupons, edge cases.
Run the tests locally. Green tests = permission to touch the effect.
4. Rewrite the effect as orchestration only
Rebuild the original
useEffectas a thin coordinator: call pure helpers, then run side effects in clearasync/awaitwithtry/catch/finally.
Resulting shape (~30 lines):
useEffect(() => {
const submitOrder = async () => {
setLoading(true);
try {
const errorMsg = validateOrder(formData);
if (errorMsg) {
showToast(errorMsg);
return;
}
const totalPrice = calculateTotalPrice(formData.items, formData.coupon);
const result = await api.post("/order/submit", { ...formData, totalPrice });
if (result.code === 200) {
showToast("Order submitted");
router.push("/success");
} else {
showToast(result.message);
}
} catch (err) {
showToast(err.message);
} finally {
setLoading(false);
}
};
if (isSubmitting) {
submitOrder();
setIsSubmitting(false);
}
}, [isSubmitting, formData /* … */]);
Readable flow: validate → price → POST → handle response.
5. Add the real feature on clean ground
Before the API call, await
riskControl.check(...). If it throws, surface the error in the existingcatch.
Inserting risk control between pricing and POST is trivial when the pipeline is linear—nightmare when it is spaghetti.
What I took away
- ~5 hours instead of ~1 week—not because AI “understood the business,” but because I decomposed the problem and verified each layer.
- AI does not replace engineers who own invariants; it punishes teams that cannot review diffs.
- Legacy code is the best AI refactor gym: greenfield hides weak process.
On Upwork this is the work I actually do—unstick React/Node codebases, add features without drama, and leave tests behind so the next change is cheaper.