How to Refactor a Large React Component Safely

Refactor a large React component without changing what users see by writing a behavior test, extracting pure functions, and splitting in small steps.

7 min read

To refactor a large React component safely, change its structure without changing what users see. A behavior test locks the current output, and small steps keep every change reviewable.

Lock behavior with a test first

Before touching the component, write a test that asserts what the user sees. The test does not care how the component is structured, so it stays green while the structure changes. It will be the safety net for every step that follows.

App.jsxApp.jsx
import { render, screen } from "@testing-library/react";
import { OrderSummary } from "./OrderSummary";
 
test("shows the total for the items", () => {
  const items = [
    { id: 1, name: "Keyboard", price: 80, quantity: 1 },
    { id: 2, name: "Mouse", price: 30, quantity: 2 },
  ];
 
  render(<OrderSummary items={items} />);
 
  expect(screen.getByText("Total: $140.00")).toBeInTheDocument();
});

The assertion targets the visible total, not an internal function or variable. That is what keeps the test valid through the refactor. The setup details are covered in React Testing Library: Test Behavior, Not Implementation.

This is why a refactor starts with a test even when the code already works: the test turns today's behavior into a contract.

Extract pure functions first

The safest change is moving calculations out of the component. A pure function has no UI and no state, so extracting it cannot change what renders. Start here because a mistake is easy to see and easy to undo.

javascriptjavascript
export function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

The component now imports and calls this function instead of computing inline. Run the test: it should still pass, because the visible total is unchanged. This is the same separation covered in How to Separate Business Logic from React UI.

Because a pure function has no render output of its own, moving it cannot change the pixels, which makes this the lowest-risk step. Formatting, validation, and totals are the usual first wins.

Extract stateful logic into a Hook

Once the pure calculations are out, look for state and effects that belong together. Move them into a custom Hook with a clear name, and have the component call the Hook.

A form component might reveal a useFormFields Hook, or a data component might reveal a useOrders Hook. The component gets shorter, and the Hook is testable on its own.

A checkout component might reveal useCheckout, which owns the cart items, the loading flag, and the submit handler, leaving the component to render three states. Keep the Hook focused on one purpose, the way useCart or useOrders reads, rather than a catch-all useComponent. Deciding whether the extraction is worth it is covered in When to Extract a Component or Custom Hook.

Split the render tree last

After logic is out, split the JSX into presentational pieces. This is the riskiest step because it moves markup, so it comes last and gets its own commit.

Split one region at a time. Extract the list, then the row, then the empty state, running the test after each. If a split breaks the test, the breakage is in the markup you just moved, which makes the fix obvious.

Move one region per commit and name each piece after what it renders. Splitting last also means the test has been protecting the logic for the whole refactor, so a markup regression is the only thing left to catch.

Run the test after every step

Each bullet is one commit, and each commit ends with a green test.

  • Write the behavior test before the first change.
  • Extract pure functions, then run the test.
  • Extract Hooks, then run the test.
  • Split the render tree one piece at a time, running the test each time.

A refactor is safe when the test is green at every intermediate point. If it is only green at the end, the refactor was not broken into small enough steps.

The dangerous move is the big-bang rewrite: deleting the component and rewriting it in one commit. Without intermediate green tests, a bug in the rewrite is indistinguishable from a deliberate behavior change.

Rune AI

Rune AI

Key Insights

  • Write a behavior test before touching the component.
  • Extract pure functions first because they cannot break the UI.
  • Move stateful logic into a custom Hook next.
  • Split the render tree last.
  • Run the test after every step, not only at the end.
RunePowered by Rune AI

Frequently Asked Questions

Do I need a full test suite before refactoring?

One behavior test that covers the component's main output is enough to start. It gives you a safety net for the first extraction, and you can add more as you go.

What order should I refactor in?

Pure functions first, then stateful logic into Hooks, then the render tree. This order moves the safest changes first and the visual changes last.

Conclusion

Refactor a large React component by locking its behavior with a test, then extracting pure functions, Hooks, and presentational pieces in small steps. Run the test after every step so behavior never changes.