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.
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.
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
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.
Frequently Asked Questions
Do I need a full test suite before refactoring?
What order should I refactor in?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.