Mingqi Hou

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:

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, no setState. 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 validateOrder and calculateTotalPrice. 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 useEffect as a thin coordinator: call pure helpers, then run side effects in clear async/await with try/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 existing catch.

Inserting risk control between pricing and POST is trivial when the pipeline is linear—nightmare when it is spaghetti.

What I took away

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.